Settings refactor (#38367)

Conrad Irwin , Ben Kunkle , Mikayla Maki , and Anthony created

Co-Authored-By: Ben K <ben@zed.dev>
Co-Authored-By: Anthony <anthony@zed.dev>
Co-Authored-By: Mikayla <mikayla@zed.dev>

Release Notes:

- settings: Major internal changes to settings. The primary user-facing
effect is that some settings which did not make sense in project
settings files are no-longer read from there. (For example the inline
blame settings)

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>

Change summary

Cargo.lock                                                                |  299 
Cargo.toml                                                                |    2 
assets/settings/default.json                                              |   83 
crates/agent/src/agent_profile.rs                                         |    4 
crates/agent/src/thread.rs                                                |    4 
crates/agent2/src/agent.rs                                                |   21 
crates/agent2/src/thread.rs                                               |    7 
crates/agent2/src/tools/edit_file_tool.rs                                 |   49 
crates/agent2/src/tools/grep_tool.rs                                      |   15 
crates/agent2/src/tools/list_directory_tool.rs                            |   14 
crates/agent2/src/tools/read_file_tool.rs                                 |   13 
crates/agent_servers/src/claude.rs                                        |    9 
crates/agent_servers/src/custom.rs                                        |   10 
crates/agent_settings/src/agent_profile.rs                                |   97 
crates/agent_settings/src/agent_settings.rs                               |  580 
crates/agent_ui/src/acp/thread_view.rs                                    |    4 
crates/agent_ui/src/agent_configuration.rs                                |  175 
crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs         |   28 
crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs |   21 
crates/agent_ui/src/agent_configuration/tool_picker.rs                    |   19 
crates/agent_ui/src/agent_model_selector.rs                               |   15 
crates/agent_ui/src/agent_panel.rs                                        |   62 
crates/agent_ui/src/agent_ui.rs                                           |    4 
crates/agent_ui/src/context_server_configuration.rs                       |    4 
crates/agent_ui/src/profile_selector.rs                                   |   20 
crates/agent_ui/src/text_thread_editor.rs                                 |   22 
crates/anthropic/Cargo.toml                                               |    1 
crates/anthropic/src/anthropic.rs                                         |   19 
crates/assistant_tools/src/edit_file_tool.rs                              |   51 
crates/assistant_tools/src/grep_tool.rs                                   |   15 
crates/assistant_tools/src/list_directory_tool.rs                         |   14 
crates/assistant_tools/src/read_file_tool.rs                              |   15 
crates/audio/Cargo.toml                                                   |    1 
crates/audio/src/audio_settings.rs                                        |   61 
crates/auto_update/Cargo.toml                                             |    1 
crates/auto_update/src/auto_update.rs                                     |   51 
crates/call/Cargo.toml                                                    |    1 
crates/call/src/call_settings.rs                                          |   44 
crates/client/src/client.rs                                               |  104 
crates/collab/src/tests/editor_tests.rs                                   |  140 
crates/collab/src/tests/following_tests.rs                                |    6 
crates/collab/src/tests/integration_tests.rs                              |   67 
crates/collab/src/tests/remote_editing_collaboration_tests.rs             |   41 
crates/collab_ui/Cargo.toml                                               |    1 
crates/collab_ui/src/collab_panel.rs                                      |    8 
crates/collab_ui/src/collab_ui.rs                                         |    2 
crates/collab_ui/src/notification_panel.rs                                |    8 
crates/collab_ui/src/panel_settings.rs                                    |  120 
crates/context_server/Cargo.toml                                          |    3 
crates/context_server/src/context_server.rs                               |   31 
crates/copilot/src/copilot_completion_provider.rs                         |   31 
crates/dap/src/debugger_settings.rs                                       |   80 
crates/debugger_ui/src/debugger_panel.rs                                  |   22 
crates/edit_prediction_button/src/edit_prediction_button.rs               |   44 
crates/editor/src/code_completion_tests.rs                                |    3 
crates/editor/src/code_context_menus.rs                                   |    2 
crates/editor/src/display_map.rs                                          |   22 
crates/editor/src/editor.rs                                               |   26 
crates/editor/src/editor_settings.rs                                      |  887 
crates/editor/src/editor_settings_controls.rs                             |   89 
crates/editor/src/editor_tests.rs                                         |  175 
crates/editor/src/element.rs                                              |   54 
crates/editor/src/hover_links.rs                                          |   20 
crates/editor/src/hover_popover.rs                                        |   20 
crates/editor/src/inlay_hint_cache.rs                                     |  317 
crates/editor/src/jsx_tag_auto_close.rs                                   |   17 
crates/eval/src/eval.rs                                                   |    5 
crates/extension_host/Cargo.toml                                          |    1 
crates/extension_host/src/extension_settings.rs                           |   34 
crates/extensions_ui/src/extension_version_selector.rs                    |    9 
crates/extensions_ui/src/extensions_ui.rs                                 |   16 
crates/file_finder/src/file_finder.rs                                     |   12 
crates/file_finder/src/file_finder_settings.rs                            |   74 
crates/git_hosting_providers/Cargo.toml                                   |    1 
crates/git_hosting_providers/src/settings.rs                              |   47 
crates/git_ui/src/blame_ui.rs                                             |    3 
crates/git_ui/src/branch_picker.rs                                        |    1 
crates/git_ui/src/git_panel.rs                                            |   16 
crates/git_ui/src/git_panel_settings.rs                                   |  121 
crates/go_to_line/Cargo.toml                                              |    2 
crates/go_to_line/src/cursor_position.rs                                  |   48 
crates/google_ai/Cargo.toml                                               |    1 
crates/google_ai/src/google_ai.rs                                         |   11 
crates/image_viewer/Cargo.toml                                            |    1 
crates/image_viewer/src/image_viewer_settings.rs                          |   42 
crates/journal/Cargo.toml                                                 |    2 
crates/journal/src/journal.rs                                             |   60 
crates/language/src/buffer.rs                                             |   19 
crates/language/src/buffer_tests.rs                                       |    6 
crates/language/src/language.rs                                           |    2 
crates/language/src/language_registry.rs                                  |    9 
crates/language/src/language_settings.rs                                  | 1037 
crates/language_model/Cargo.toml                                          |    1 
crates/language_model/src/language_model.rs                               |    9 
crates/language_models/src/provider/anthropic.rs                          |   55 
crates/language_models/src/provider/bedrock.rs                            |   19 
crates/language_models/src/provider/cloud.rs                              |   38 
crates/language_models/src/provider/deepseek.rs                           |   12 
crates/language_models/src/provider/google.rs                             |   29 
crates/language_models/src/provider/lmstudio.rs                           |   12 
crates/language_models/src/provider/mistral.rs                            |   15 
crates/language_models/src/provider/ollama.rs                             |   50 
crates/language_models/src/provider/open_ai.rs                            |   14 
crates/language_models/src/provider/open_ai_compatible.rs                 |   34 
crates/language_models/src/provider/open_router.rs                        |   54 
crates/language_models/src/provider/vercel.rs                             |   14 
crates/language_models/src/provider/x_ai.rs                               |   12 
crates/language_models/src/settings.rs                                    |  460 
crates/languages/src/bash.rs                                              |    6 
crates/languages/src/c.rs                                                 |    6 
crates/languages/src/lib.rs                                               |    7 
crates/languages/src/python.rs                                            |    6 
crates/languages/src/rust.rs                                              |    5 
crates/markdown/examples/markdown.rs                                      |    5 
crates/markdown/examples/markdown_as_child.rs                             |    5 
crates/multi_buffer/src/multi_buffer.rs                                   |   12 
crates/ollama/Cargo.toml                                                  |    1 
crates/ollama/src/ollama.rs                                               |   24 
crates/onboarding/Cargo.toml                                              |    3 
crates/onboarding/src/ai_setup_page.rs                                    |    4 
crates/onboarding/src/base_keymap_picker.rs                               |    4 
crates/onboarding/src/basics_page.rs                                      |   30 
crates/onboarding/src/editing_page.rs                                     |   67 
crates/open_ai/Cargo.toml                                                 |    1 
crates/open_ai/src/open_ai.rs                                             |   11 
crates/open_router/Cargo.toml                                             |    4 
crates/open_router/src/open_router.rs                                     |   50 
crates/outline_panel/Cargo.toml                                           |    1 
crates/outline_panel/src/outline_panel.rs                                 |   24 
crates/outline_panel/src/outline_panel_settings.rs                        |  166 
crates/project/src/agent_server_store.rs                                  |  114 
crates/project/src/context_server_store.rs                                |   35 
crates/project/src/debugger/dap_store.rs                                  |   15 
crates/project/src/lsp_store.rs                                           |    3 
crates/project/src/project.rs                                             |  214 
crates/project/src/project_settings.rs                                    |  725 
crates/project/src/project_tests.rs                                       |   20 
crates/project_panel/src/project_panel.rs                                 |   45 
crates/project_panel/src/project_panel_settings.rs                        |  252 
crates/project_panel/src/project_panel_tests.rs                           |   81 
crates/recent_projects/Cargo.toml                                         |    1 
crates/recent_projects/src/recent_projects.rs                             |    9 
crates/recent_projects/src/remote_connections.rs                          |  116 
crates/recent_projects/src/remote_servers.rs                              |   78 
crates/remote/Cargo.toml                                                  |    2 
crates/remote/src/transport/ssh.rs                                        |   24 
crates/remote/src/transport/wsl.rs                                        |    9 
crates/repl/Cargo.toml                                                    |    1 
crates/repl/src/jupyter_settings.rs                                       |   53 
crates/repl/src/repl_settings.rs                                          |   47 
crates/search/src/buffer_search.rs                                        |   12 
crates/settings/Cargo.toml                                                |    2 
crates/settings/src/base_keymap_setting.rs                                |   65 
crates/settings/src/editable_setting_control.rs                           |   13 
crates/settings/src/settings.rs                                           |   11 
crates/settings/src/settings_content.rs                                   |  818 
crates/settings/src/settings_content/agent.rs                             |  336 
crates/settings/src/settings_content/editor.rs                            |  603 
crates/settings/src/settings_content/language.rs                          |  894 
crates/settings/src/settings_content/language_model.rs                    |  419 
crates/settings/src/settings_content/project.rs                           |  435 
crates/settings/src/settings_content/terminal.rs                          |  318 
crates/settings/src/settings_content/theme.rs                             | 1076 
crates/settings/src/settings_content/workspace.rs                         |  440 
crates/settings/src/settings_file.rs                                      |    8 
crates/settings/src/settings_json.rs                                      |    9 
crates/settings/src/settings_store.rs                                     |  781 
crates/settings/src/vscode_import.rs                                      |   11 
crates/settings_ui/Cargo.toml                                             |   42 
crates/settings_ui/LICENSE-GPL                                            |    1 
crates/settings_ui/src/appearance_settings_controls.rs                    |  387 
crates/settings_ui/src/settings_ui.rs                                     | 1017 
crates/terminal/src/terminal_settings.rs                                  |  437 
crates/terminal_view/src/terminal_panel.rs                                |   27 
crates/terminal_view/src/terminal_view.rs                                 |   11 
crates/theme/Cargo.toml                                                   |    1 
crates/theme/src/schema.rs                                                | 2225 
crates/theme/src/settings.rs                                              |  794 
crates/theme/src/styles/accents.rs                                        |    6 
crates/theme/src/styles/players.rs                                        |    6 
crates/theme/src/theme.rs                                                 |    9 
crates/theme_importer/Cargo.toml                                          |    1 
crates/theme_importer/src/main.rs                                         |    4 
crates/theme_importer/src/vscode/converter.rs                             |    8 
crates/theme_selector/src/icon_theme_selector.rs                          |    4 
crates/theme_selector/src/theme_selector.rs                               |    4 
crates/title_bar/src/title_bar_settings.rs                                |   95 
crates/ui/src/components/scrollbar.rs                                     |   11 
crates/ui_macros/src/dynamic_spacing.rs                                   |   12 
crates/util/src/util.rs                                                   |   18 
crates/vim/src/digraph.rs                                                 |    5 
crates/vim/src/normal.rs                                                  |   15 
crates/vim/src/normal/paste.rs                                            |   39 
crates/vim/src/normal/scroll.rs                                           |   15 
crates/vim/src/normal/search.rs                                           |    5 
crates/vim/src/test.rs                                                    |    5 
crates/vim/src/test/neovim_backed_test_context.rs                         |   13 
crates/vim/src/test/vim_test_context.rs                                   |   10 
crates/vim/src/vim.rs                                                     |  139 
crates/vim_mode_setting/Cargo.toml                                        |    3 
crates/vim_mode_setting/src/vim_mode_setting.rs                           |   99 
crates/workspace/src/dock.rs                                              |   25 
crates/workspace/src/item.rs                                              |  212 
crates/workspace/src/pane.rs                                              |   13 
crates/workspace/src/workspace.rs                                         |   47 
crates/workspace/src/workspace_settings.rs                                |  458 
crates/worktree/Cargo.toml                                                |    1 
crates/worktree/src/worktree_settings.rs                                  |  136 
crates/worktree/src/worktree_tests.rs                                     |   39 
crates/zed/Cargo.toml                                                     |    2 
crates/zed/src/main.rs                                                    |    1 
crates/zed/src/zed.rs                                                     |   68 
crates/zeta/src/init.rs                                                   |   19 
crates/zeta/src/onboarding_modal.rs                                       |    6 
crates/zeta_cli/src/headless.rs                                           |    5 
crates/zlog/Cargo.toml                                                    |    1 
crates/zlog/src/filter.rs                                                 |   11 
crates/zlog_settings/Cargo.toml                                           |    4 
crates/zlog_settings/src/zlog_settings.rs                                 |   45 
219 files changed, 10,893 insertions(+), 10,808 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -174,7 +174,7 @@ dependencies = [
  "rand 0.9.1",
  "ref-cast",
  "rope",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "settings",
@@ -205,7 +205,7 @@ dependencies = [
  "futures 0.3.31",
  "log",
  "parking_lot",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
 ]
@@ -257,7 +257,7 @@ dependencies = [
  "prompt_store",
  "reqwest_client",
  "rust-embed",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "settings",
@@ -337,7 +337,7 @@ dependencies = [
  "gpui",
  "language_model",
  "paths",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -412,7 +412,7 @@ dependencies = [
  "release_channel",
  "rope",
  "rules_library",
- "schemars",
+ "schemars 1.0.1",
  "search",
  "serde",
  "serde_json",
@@ -656,9 +656,10 @@ dependencies = [
  "chrono",
  "futures 0.3.31",
  "http_client",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
+ "settings",
  "strum 0.27.1",
  "thiserror 2.0.12",
  "workspace-hack",
@@ -1007,7 +1008,7 @@ dependencies = [
  "regex",
  "reqwest_client",
  "rust-embed",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "settings",
@@ -1409,7 +1410,6 @@ dependencies = [
  "log",
  "parking_lot",
  "rodio",
- "schemars",
  "serde",
  "settings",
  "smol",
@@ -1442,7 +1442,6 @@ dependencies = [
  "log",
  "paths",
  "release_channel",
- "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -2144,7 +2143,7 @@ dependencies = [
  "aws-sdk-bedrockruntime",
  "aws-smithy-types",
  "futures 0.3.31",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "strum 0.27.1",
@@ -2645,7 +2644,6 @@ dependencies = [
  "log",
  "postage",
  "project",
- "schemars",
  "serde",
  "settings",
  "telemetry",
@@ -2868,7 +2866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff"
 dependencies = [
  "heck 0.4.1",
- "indexmap",
+ "indexmap 2.9.0",
  "log",
  "proc-macro2",
  "quote",
@@ -3146,7 +3144,7 @@ dependencies = [
  "release_channel",
  "rpc",
  "rustls-pki-types",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "serde_urlencoded",
@@ -3457,7 +3455,6 @@ dependencies = [
  "project",
  "release_channel",
  "rpc",
- "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -3479,7 +3476,7 @@ dependencies = [
 name = "collections"
 version = "0.1.0"
 dependencies = [
- "indexmap",
+ "indexmap 2.9.0",
  "rustc-hash 2.1.1",
  "workspace-hack",
 ]
@@ -3641,9 +3638,10 @@ dependencies = [
  "net",
  "parking_lot",
  "postage",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
+ "settings",
  "smol",
  "tempfile",
  "url",
@@ -4441,7 +4439,7 @@ dependencies = [
  "parking_lot",
  "paths",
  "proto",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "settings",
@@ -4461,7 +4459,7 @@ name = "dap-types"
 version = "0.0.1"
 source = "git+https://github.com/zed-industries/dap-types?rev=1b461b310481d01e02b2603c16d7144b926339f8#1b461b310481d01e02b2603c16d7144b926339f8"
 dependencies = [
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
 ]
@@ -4491,6 +4489,41 @@ dependencies = [
  "workspace-hack",
 ]
 
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.101",
+]
+
 [[package]]
 name = "dashmap"
 version = "5.5.3"
@@ -4632,7 +4665,7 @@ dependencies = [
  "pretty_assertions",
  "project",
  "rpc",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -4673,7 +4706,7 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "workspace-hack",
@@ -5207,7 +5240,7 @@ dependencies = [
  "regex",
  "release_channel",
  "rpc",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "settings",
@@ -5714,7 +5747,6 @@ dependencies = [
  "release_channel",
  "remote",
  "reqwest_client",
- "schemars",
  "semantic_version",
  "serde",
  "serde_json",
@@ -5911,7 +5943,7 @@ dependencies = [
  "picker",
  "pretty_assertions",
  "project",
- "schemars",
+ "schemars 1.0.1",
  "search",
  "serde",
  "serde_json",
@@ -6770,7 +6802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 dependencies = [
  "fallible-iterator",
- "indexmap",
+ "indexmap 2.9.0",
  "stable_deref_trait",
 ]
 
@@ -6793,7 +6825,7 @@ dependencies = [
  "rand 0.9.1",
  "regex",
  "rope",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "smol",
@@ -6835,7 +6867,6 @@ dependencies = [
  "indoc",
  "pretty_assertions",
  "regex",
- "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -6880,7 +6911,7 @@ dependencies = [
  "postage",
  "pretty_assertions",
  "project",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "settings",
@@ -7707,7 +7738,6 @@ dependencies = [
 name = "go_to_line"
 version = "0.1.0"
 dependencies = [
- "anyhow",
  "editor",
  "gpui",
  "indoc",
@@ -7715,7 +7745,6 @@ dependencies = [
  "menu",
  "project",
  "rope",
- "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -7747,9 +7776,10 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
+ "settings",
  "strum 0.27.1",
  "workspace-hack",
 ]
@@ -7848,7 +7878,7 @@ dependencies = [
  "reqwest_client",
  "resvg",
  "scap",
- "schemars",
+ "schemars 1.0.1",
  "seahash",
  "semantic_version",
  "serde",
@@ -7934,7 +7964,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http 0.2.12",
- "indexmap",
+ "indexmap 2.9.0",
  "slab",
  "tokio",
  "tokio-util",
@@ -7953,7 +7983,7 @@ dependencies = [
  "futures-core",
  "futures-sink",
  "http 1.3.1",
- "indexmap",
+ "indexmap 2.9.0",
  "slab",
  "tokio",
  "tokio-util",
@@ -8652,6 +8682,12 @@ version = "2.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
 
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
 [[package]]
 name = "idna"
 version = "1.0.3"
@@ -8734,7 +8770,6 @@ dependencies = [
  "language",
  "log",
  "project",
- "schemars",
  "serde",
  "settings",
  "theme",
@@ -8765,6 +8800,17 @@ version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
 
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
 [[package]]
 name = "indexmap"
 version = "2.9.0"
@@ -9129,7 +9175,7 @@ dependencies = [
  "hashbrown 0.15.3",
  "hex",
  "ignore",
- "indexmap",
+ "indexmap 2.9.0",
  "interim",
  "itertools 0.14.0",
  "jj-lib-proc-macros",
@@ -9226,10 +9272,10 @@ dependencies = [
  "editor",
  "gpui",
  "log",
- "schemars",
  "serde",
  "settings",
  "shellexpand 2.1.2",
+ "util",
  "workspace",
  "workspace-hack",
 ]
@@ -9460,7 +9506,7 @@ dependencies = [
  "rand 0.9.1",
  "regex",
  "rpc",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "settings",
@@ -9534,9 +9580,10 @@ dependencies = [
  "open_router",
  "parking_lot",
  "proto",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
+ "settings",
  "smol",
  "telemetry_events",
  "thiserror 2.0.12",
@@ -9583,7 +9630,7 @@ dependencies = [
  "partial-json-fixer",
  "project",
  "release_channel",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "settings",
@@ -9699,7 +9746,7 @@ dependencies = [
  "regex",
  "rope",
  "rust-embed",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -10113,7 +10160,7 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "workspace-hack",
@@ -10219,7 +10266,7 @@ dependencies = [
  "parking_lot",
  "postage",
  "release_channel",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "smol",
@@ -10757,7 +10804,7 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "strum 0.27.1",
@@ -10844,7 +10891,7 @@ dependencies = [
  "half",
  "hashbrown 0.15.3",
  "hexf-parse",
- "indexmap",
+ "indexmap 2.9.0",
  "log",
  "num-traits",
  "once_cell",
@@ -11503,7 +11550,7 @@ checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
 dependencies = [
  "crc32fast",
  "hashbrown 0.15.3",
- "indexmap",
+ "indexmap 2.9.0",
  "memchr",
 ]
 
@@ -11514,9 +11561,10 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
+ "settings",
  "workspace-hack",
 ]
 
@@ -11542,7 +11590,7 @@ dependencies = [
  "notifications",
  "picker",
  "project",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "settings",
  "telemetry",
@@ -11622,9 +11670,10 @@ dependencies = [
  "futures 0.3.31",
  "http_client",
  "log",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
+ "settings",
  "strum 0.27.1",
  "workspace-hack",
 ]
@@ -11636,12 +11685,12 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
+ "settings",
  "strum 0.27.1",
  "thiserror 2.0.12",
- "util",
  "workspace-hack",
 ]
 
@@ -11815,7 +11864,6 @@ dependencies = [
  "outline",
  "pretty_assertions",
  "project",
- "schemars",
  "search",
  "serde",
  "serde_json",
@@ -12516,7 +12564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
 dependencies = [
  "fixedbitset",
- "indexmap",
+ "indexmap 2.9.0",
 ]
 
 [[package]]
@@ -12590,7 +12638,7 @@ dependencies = [
  "env_logger 0.11.8",
  "gpui",
  "menu",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "ui",
@@ -12697,7 +12745,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d"
 dependencies = [
  "base64 0.22.1",
- "indexmap",
+ "indexmap 2.9.0",
  "quick-xml 0.32.0",
  "serde",
  "time",
@@ -13027,7 +13075,7 @@ dependencies = [
  "gpui",
  "http_client",
  "image",
- "indexmap",
+ "indexmap 2.9.0",
  "itertools 0.14.0",
  "language",
  "log",
@@ -13045,7 +13093,7 @@ dependencies = [
  "release_channel",
  "remote",
  "rpc",
- "schemars",
+ "schemars 1.0.1",
  "semver",
  "serde",
  "serde_json",
@@ -13087,12 +13135,12 @@ dependencies = [
  "git",
  "git_ui",
  "gpui",
- "indexmap",
+ "indexmap 2.9.0",
  "language",
  "menu",
  "pretty_assertions",
  "project",
- "schemars",
+ "schemars 1.0.1",
  "search",
  "serde",
  "serde_json",
@@ -13787,7 +13835,6 @@ dependencies = [
  "project",
  "release_channel",
  "remote",
- "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -13961,9 +14008,9 @@ dependencies = [
  "prost 0.9.0",
  "release_channel",
  "rpc",
- "schemars",
  "serde",
  "serde_json",
+ "settings",
  "shlex",
  "smol",
  "tempfile",
@@ -14082,7 +14129,6 @@ dependencies = [
  "picker",
  "project",
  "runtimelib",
- "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -14899,13 +14945,25 @@ dependencies = [
  "anyhow",
  "clap",
  "env_logger 0.11.8",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "theme",
  "workspace-hack",
 ]
 
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "schemars"
 version = "1.0.1"
@@ -14913,7 +14971,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
 dependencies = [
  "dyn-clone",
- "indexmap",
+ "indexmap 2.9.0",
  "ref-cast",
  "schemars_derive",
  "serde",
@@ -15124,7 +15182,7 @@ dependencies = [
  "language",
  "menu",
  "project",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "settings",
@@ -15274,7 +15332,7 @@ version = "1.0.144"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56177480b00303e689183f110b4e727bb4211d692c62d4fcd16d02be93077d40"
 dependencies = [
- "indexmap",
+ "indexmap 2.9.0",
  "itoa",
  "memchr",
  "ryu",
@@ -15287,7 +15345,7 @@ version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540"
 dependencies = [
- "indexmap",
+ "indexmap 2.9.0",
  "itoa",
  "memchr",
  "ryu",
@@ -15336,6 +15394,37 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_with"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.9.0",
+ "schemars 0.9.0",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
 [[package]]
 name = "serial2"
 version = "0.2.29"
@@ -15376,11 +15465,13 @@ dependencies = [
  "pretty_assertions",
  "release_channel",
  "rust-embed",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "serde_json_lenient",
  "serde_path_to_error",
+ "serde_repr",
+ "serde_with",
  "settings_ui_macros",
  "smallvec",
  "tree-sitter",
@@ -15412,27 +15503,6 @@ dependencies = [
  "zed_actions",
 ]
 
-[[package]]
-name = "settings_ui"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "command_palette_hooks",
- "debugger_ui",
- "editor",
- "feature_flags",
- "gpui",
- "menu",
- "serde",
- "serde_json",
- "settings",
- "smallvec",
- "theme",
- "ui",
- "workspace",
- "workspace-hack",
-]
-
 [[package]]
 name = "settings_ui_macros"
 version = "0.1.0"
@@ -15730,7 +15800,7 @@ dependencies = [
  "indoc",
  "parking_lot",
  "paths",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json_lenient",
  "snippet",
@@ -15891,7 +15961,7 @@ dependencies = [
  "futures-util",
  "hashbrown 0.15.3",
  "hashlink 0.10.0",
- "indexmap",
+ "indexmap 2.9.0",
  "log",
  "memchr",
  "once_cell",
@@ -16810,7 +16880,7 @@ dependencies = [
  "menu",
  "picker",
  "project",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "settings",
@@ -16890,7 +16960,7 @@ dependencies = [
  "parking_lot",
  "pretty_assertions",
  "proto",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -16996,7 +17066,7 @@ dependencies = [
  "rand 0.9.1",
  "regex",
  "release_channel",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "settings",
  "smol",
@@ -17042,7 +17112,7 @@ dependencies = [
  "project",
  "rand 0.9.1",
  "regex",
- "schemars",
+ "schemars 1.0.1",
  "search",
  "serde",
  "serde_json",
@@ -17092,17 +17162,16 @@ dependencies = [
  "fs",
  "futures 0.3.31",
  "gpui",
- "indexmap",
+ "indexmap 2.9.0",
  "inventory",
  "log",
  "palette",
  "parking_lot",
  "refineable",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "serde_json_lenient",
- "serde_repr",
  "settings",
  "strum 0.27.1",
  "thiserror 2.0.12",
@@ -17129,8 +17198,9 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "clap",
+ "collections",
  "gpui",
- "indexmap",
+ "indexmap 2.9.0",
  "log",
  "palette",
  "serde",
@@ -17384,7 +17454,7 @@ dependencies = [
  "project",
  "remote",
  "rpc",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "settings",
  "smallvec",
@@ -17583,7 +17653,7 @@ version = "0.22.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
 dependencies = [
- "indexmap",
+ "indexmap 2.9.0",
  "serde",
  "serde_spanned",
  "toml_datetime",
@@ -18194,7 +18264,7 @@ dependencies = [
  "icons",
  "itertools 0.14.0",
  "menu",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "settings",
  "smallvec",
@@ -18469,7 +18539,7 @@ dependencies = [
  "rand 0.9.1",
  "regex",
  "rust-embed",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -18579,7 +18649,7 @@ name = "vercel"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "schemars",
+ "schemars 1.0.1",
  "serde",
  "strum 0.27.1",
  "workspace-hack",
@@ -18627,7 +18697,7 @@ dependencies = [
  "project_panel",
  "regex",
  "release_channel",
- "schemars",
+ "schemars 1.0.1",
  "search",
  "serde",
  "serde_json",
@@ -18648,10 +18718,7 @@ dependencies = [
 name = "vim_mode_setting"
 version = "0.1.0"
 dependencies = [
- "anyhow",
  "gpui",
- "schemars",
- "serde",
  "settings",
  "workspace-hack",
 ]
@@ -18886,7 +18953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2"
 dependencies = [
  "anyhow",
- "indexmap",
+ "indexmap 2.9.0",
  "serde",
  "serde_derive",
  "serde_json",
@@ -18904,7 +18971,7 @@ dependencies = [
  "anyhow",
  "auditable-serde",
  "flate2",
- "indexmap",
+ "indexmap 2.9.0",
  "serde",
  "serde_derive",
  "serde_json",
@@ -18934,7 +19001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
 dependencies = [
  "bitflags 2.9.0",
- "indexmap",
+ "indexmap 2.9.0",
  "semver",
 ]
 
@@ -18946,7 +19013,7 @@ checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185"
 dependencies = [
  "bitflags 2.9.0",
  "hashbrown 0.15.3",
- "indexmap",
+ "indexmap 2.9.0",
  "semver",
  "serde",
 ]
@@ -18959,7 +19026,7 @@ checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2"
 dependencies = [
  "bitflags 2.9.0",
  "hashbrown 0.15.3",
- "indexmap",
+ "indexmap 2.9.0",
  "semver",
 ]
 
@@ -18988,7 +19055,7 @@ dependencies = [
  "cfg-if",
  "encoding_rs",
  "hashbrown 0.14.5",
- "indexmap",
+ "indexmap 2.9.0",
  "libc",
  "log",
  "mach2 0.4.2",
@@ -19112,7 +19179,7 @@ dependencies = [
  "cranelift-bitset",
  "cranelift-entity",
  "gimli",
- "indexmap",
+ "indexmap 2.9.0",
  "log",
  "object",
  "postcard",
@@ -19237,7 +19304,7 @@ checksum = "8358319c2dd1e4db79e3c1c5d3a5af84956615343f9f89f4e4996a36816e06e6"
 dependencies = [
  "anyhow",
  "heck 0.5.0",
- "indexmap",
+ "indexmap 2.9.0",
  "wit-parser 0.221.3",
 ]
 
@@ -20348,7 +20415,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3"
 dependencies = [
  "anyhow",
  "heck 0.4.1",
- "indexmap",
+ "indexmap 2.9.0",
  "wasm-metadata 0.201.0",
  "wit-bindgen-core 0.22.0",
  "wit-component 0.201.0",

Cargo.toml πŸ”—

@@ -150,7 +150,6 @@ members = [
     "crates/session",
     "crates/settings",
     "crates/settings_profile_selector",
-    "crates/settings_ui",
     "crates/settings_ui_macros",
     "crates/snippet",
     "crates/snippet_provider",
@@ -630,6 +629,7 @@ serde_json_lenient = { version = "0.2", features = [
 serde_path_to_error = "0.1.17"
 serde_repr = "0.1"
 serde_urlencoded = "0.7"
+serde_with = "3.4.0"
 sha2 = "0.10"
 shellexpand = "2.1.0"
 shlex = "1.3.0"

assets/settings/default.json πŸ”—

@@ -311,7 +311,7 @@
   // bracket, brace, single or double quote characters.
   // For example, when you select text and type (, Zed will surround the text with ().
   "use_auto_surround": true,
-  /// Whether indentation should be adjusted based on the context whilst typing.
+  // Whether indentation should be adjusted based on the context whilst typing.
   "auto_indent": true,
   // Whether indentation of pasted content should be adjusted based on the context.
   "auto_indent_on_paste": true,
@@ -408,6 +408,21 @@
     // Whether to show the menus in the titlebar.
     "show_menus": false
   },
+  "audio": {
+    // Opt into the new audio system.
+    "experimental.rodio_audio": false,
+    // Requires 'rodio_audio: true'
+    //
+    // Use the new audio systems automatic gain control for your microphone.
+    // This affects how loud you sound to others.
+    "experimental.control_input_volume": false,
+    // Requires 'rodio_audio: true'
+    //
+    // Use the new audio systems automatic gain control on everyone in the
+    // call. This makes call members who are too quite louder and those who are
+    // too loud quieter. This only affects how things sound for you.
+    "experimental.control_output_volume": false
+  },
   // Scrollbar related settings
   "scrollbar": {
     // When to show the scrollbar in the editor.
@@ -588,6 +603,7 @@
     // Toggle certain types of hints on and off, all switched on by default.
     "show_type_hints": true,
     "show_parameter_hints": true,
+    "show_value_hints": true,
     // Corresponds to null/None LSP hint type value.
     "show_other_hints": true,
     // Whether to show a background for inlay hints.
@@ -796,7 +812,7 @@
   "agent": {
     // Whether the agent is enabled.
     "enabled": true,
-    /// What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
+    // What completion mode to start new threads in, if available. Can be 'normal' or 'burn'.
     "preferred_completion_mode": "normal",
     // Whether to show the agent panel button in the status bar.
     "button": true,
@@ -806,6 +822,8 @@
     "default_width": 640,
     // Default height when the agent panel is docked to the bottom.
     "default_height": 320,
+    // The view to use by default (thread, or text_thread)
+    "default_view": "thread",
     // The default model to use when creating new threads.
     "default_model": {
       // The provider to use.
@@ -907,14 +925,18 @@
 
     // Default: false
     "play_sound_when_agent_done": false,
-    /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
-    ///
-    /// Default: true
+    // Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
+    //
+    // Default: true
     "expand_edit_card": true,
-    /// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
-    ///
-    /// Default: true
+    // Whether to have terminal cards in the agent panel expanded, showing the whole command output.
+    //
+    // Default: true
     "expand_terminal_card": true,
+    // Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
+    //
+    // Default: false
+    "use_modifier_to_send": false,
     // Minimum number of lines to display in the agent message editor.
     //
     // Default: 4
@@ -931,6 +953,7 @@
   //
   // This is typically customized on a per-language basis.
   "language_servers": ["..."],
+
   // When to automatically save edited buffers. This setting can
   // take four values.
   //
@@ -1262,7 +1285,13 @@
     // },
     // Whether edit predictions are enabled when editing text threads.
     // This setting has no effect if globally disabled.
-    "enabled_in_text_threads": true
+    "enabled_in_text_threads": true,
+
+    "copilot": {
+      "enterprise_uri": null,
+      "proxy": null,
+      "proxy_no_verify": null
+    }
   },
   // Settings specific to journaling
   "journal": {
@@ -1765,6 +1794,7 @@
     "anthropic": {
       "api_url": "https://api.anthropic.com"
     },
+    "bedrock": {},
     "google": {
       "api_url": "https://generativelanguage.googleapis.com"
     },
@@ -1786,14 +1816,30 @@
     },
     "mistral": {
       "api_url": "https://api.mistral.ai/v1"
-    }
+    },
+    "vercel": {
+      "api_url": "https://api.v0.dev/v1"
+    },
+    "x_ai": {
+      "api_url": "https://api.x.ai/v1"
+    },
+    "zed.dev": {}
+  },
+  "session": {
+    // Whether or not to restore unsaved buffers on restart.
+    //
+    // If this is true, user won't be prompted whether to save/discard
+    // dirty files when closing the application.
+    //
+    // Default: true
+    "restore_unsaved_buffers": true
   },
   // Zed's Prettier integration settings.
   // Allows to enable/disable formatting with Prettier
   // and configure default Prettier, used when no project-level Prettier installation is found.
   "prettier": {
     // // Whether to consider prettier formatter or not when attempting to format a file.
-    // "allowed": false,
+    "allowed": false
     //
     // // Use regular Prettier json configuration.
     // // If Prettier is allowed, Zed will use this for its Prettier instance for any applicable file, if
@@ -1826,6 +1872,10 @@
     //     }
     // }
   },
+  // DAP Specific settings.
+  "dap": {
+    // Specify the DAP name as a key here.
+  },
   // Common language server settings.
   "global_lsp_settings": {
     // Whether to show the LSP servers button in the status bar.
@@ -1833,7 +1883,8 @@
   },
   // Jupyter settings
   "jupyter": {
-    "enabled": true
+    "enabled": true,
+    "kernel_selections": {}
     // Specify the language name as the key and the kernel name as the value.
     // "kernel_selections": {
     //    "python": "conda-base"
@@ -1964,5 +2015,11 @@
   //     }
   //   }
   // }
-  "profiles": []
+  "profiles": [],
+
+  // A map of log scopes to the desired log level.
+  // Useful for filtering out noisy logs or enabling more verbose logging.
+  //
+  // Example: {"log": {"client": "warn"}}
+  "log": {}
 }

crates/agent/src/agent_profile.rs πŸ”—

@@ -49,10 +49,10 @@ impl AgentProfile {
                 .unwrap_or_default(),
         };
 
-        update_settings_file::<AgentSettings>(fs, cx, {
+        update_settings_file(fs, cx, {
             let id = id.clone();
             move |settings, _cx| {
-                settings.create_profile(id, profile_settings).log_err();
+                profile_settings.save_to_settings(id, settings).log_err();
             }
         });
 

crates/agent/src/thread.rs πŸ”—

@@ -3272,7 +3272,7 @@ mod tests {
 
     // Test-specific constants
     const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30;
-    use agent_settings::{AgentProfileId, AgentSettings, LanguageModelParameters};
+    use agent_settings::{AgentProfileId, AgentSettings};
     use assistant_tool::ToolRegistry;
     use assistant_tools;
     use futures::StreamExt;
@@ -3289,7 +3289,7 @@ mod tests {
     use project::{FakeFs, Project};
     use prompt_store::PromptBuilder;
     use serde_json::json;
-    use settings::{Settings, SettingsStore};
+    use settings::{LanguageModelParameters, Settings, SettingsStore};
     use std::sync::Arc;
     use std::time::Duration;
     use theme::ThemeSettings;

crates/agent2/src/agent.rs πŸ”—

@@ -6,7 +6,6 @@ use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, Token
 use acp_thread::{AcpThread, AgentModelSelector};
 use action_log::ActionLog;
 use agent_client_protocol as acp;
-use agent_settings::AgentSettings;
 use anyhow::{Context as _, Result, anyhow};
 use collections::{HashSet, IndexMap};
 use fs::Fs;
@@ -21,7 +20,7 @@ use project::{Project, ProjectItem, ProjectPath, Worktree};
 use prompt_store::{
     ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
 };
-use settings::update_settings_file;
+use settings::{LanguageModelSelection, update_settings_file};
 use std::any::Any;
 use std::collections::HashMap;
 use std::path::{Path, PathBuf};
@@ -873,13 +872,17 @@ impl AgentModelSelector for NativeAgentConnection {
             thread.set_model(model.clone(), cx);
         });
 
-        update_settings_file::<AgentSettings>(
-            self.0.read(cx).fs.clone(),
-            cx,
-            move |settings, _cx| {
-                settings.set_model(model);
-            },
-        );
+        update_settings_file(self.0.read(cx).fs.clone(), cx, move |settings, _cx| {
+            let provider = model.provider_id().0.to_string();
+            let model = model.id().0.to_string();
+            settings
+                .agent
+                .get_or_insert_default()
+                .set_model(LanguageModelSelection {
+                    provider: provider.into(),
+                    model,
+                });
+        });
 
         Task::ready(Ok(()))
     }

crates/agent2/src/thread.rs πŸ”—

@@ -2477,8 +2477,11 @@ impl ToolCallEventStream {
             "always_allow" => {
                 if let Some(fs) = fs.clone() {
                     cx.update(|cx| {
-                        update_settings_file::<AgentSettings>(fs, cx, |settings, _| {
-                            settings.set_always_allow_tool_actions(true);
+                        update_settings_file(fs, cx, |settings, _| {
+                            settings
+                                .agent
+                                .get_or_insert_default()
+                                .set_always_allow_tool_actions(true);
                         });
                     })?;
                 }

crates/agent2/src/tools/edit_file_tool.rs πŸ”—

@@ -791,14 +791,11 @@ mod tests {
         // First, test with format_on_save enabled
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.format_on_save = Some(FormatOnSave::On);
-                        settings.defaults.formatter =
-                            Some(language::language_settings::SelectedFormatter::Auto);
-                    },
-                );
+                store.update_user_settings(cx, |settings| {
+                    settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
+                    settings.project.all_languages.defaults.formatter =
+                        Some(language::language_settings::SelectedFormatter::Auto);
+                });
             });
         });
 
@@ -853,12 +850,10 @@ mod tests {
         // Next, test with format_on_save disabled
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.format_on_save = Some(FormatOnSave::Off);
-                    },
-                );
+                store.update_user_settings(cx, |settings| {
+                    settings.project.all_languages.defaults.format_on_save =
+                        Some(FormatOnSave::Off);
+                });
             });
         });
 
@@ -935,12 +930,13 @@ mod tests {
         // First, test with remove_trailing_whitespace_on_save enabled
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.remove_trailing_whitespace_on_save = Some(true);
-                    },
-                );
+                store.update_user_settings(cx, |settings| {
+                    settings
+                        .project
+                        .all_languages
+                        .defaults
+                        .remove_trailing_whitespace_on_save = Some(true);
+                });
             });
         });
 
@@ -991,12 +987,13 @@ mod tests {
         // Next, test with remove_trailing_whitespace_on_save disabled
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.remove_trailing_whitespace_on_save = Some(false);
-                    },
-                );
+                store.update_user_settings(cx, |settings| {
+                    settings
+                        .project
+                        .all_languages
+                        .defaults
+                        .remove_trailing_whitespace_on_save = Some(false);
+                });
             });
         });
 

crates/agent2/src/tools/grep_tool.rs πŸ”—

@@ -308,7 +308,7 @@ mod tests {
     use super::*;
     use gpui::{TestAppContext, UpdateGlobal};
     use language::{Language, LanguageConfig, LanguageMatcher};
-    use project::{FakeFs, Project, WorktreeSettings};
+    use project::{FakeFs, Project};
     use serde_json::json;
     use settings::SettingsStore;
     use unindent::Unindent;
@@ -827,15 +827,14 @@ mod tests {
 
         cx.update(|cx| {
             use gpui::UpdateGlobal;
-            use project::WorktreeSettings;
             use settings::SettingsStore;
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions = Some(vec![
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions = Some(vec![
                         "**/.secretdir".to_string(),
                         "**/.mymetadata".to_string(),
                     ]);
-                    settings.private_files = Some(vec![
+                    settings.project.worktree.private_files = Some(vec![
                         "**/.mysecrets".to_string(),
                         "**/*.privatekey".to_string(),
                         "**/*.mysensitive".to_string(),
@@ -1062,10 +1061,10 @@ mod tests {
         // Set global settings
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions =
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
                 });
             });
         });

crates/agent2/src/tools/list_directory_tool.rs πŸ”—

@@ -214,7 +214,7 @@ mod tests {
     use super::*;
     use gpui::{TestAppContext, UpdateGlobal};
     use indoc::indoc;
-    use project::{FakeFs, Project, WorktreeSettings};
+    use project::{FakeFs, Project};
     use serde_json::json;
     use settings::SettingsStore;
     use util::path;
@@ -421,13 +421,13 @@ mod tests {
         // Configure settings explicitly
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions = Some(vec![
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions = Some(vec![
                         "**/.secretdir".to_string(),
                         "**/.mymetadata".to_string(),
                         "**/.hidden_subdir".to_string(),
                     ]);
-                    settings.private_files = Some(vec![
+                    settings.project.worktree.private_files = Some(vec![
                         "**/.mysecrets".to_string(),
                         "**/*.privatekey".to_string(),
                         "**/*.mysensitive".to_string(),
@@ -565,10 +565,10 @@ mod tests {
         // Set global settings
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions =
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
                 });
             });
         });

crates/agent2/src/tools/read_file_tool.rs πŸ”—

@@ -586,15 +586,14 @@ mod test {
 
         cx.update(|cx| {
             use gpui::UpdateGlobal;
-            use project::WorktreeSettings;
             use settings::SettingsStore;
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions = Some(vec![
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions = Some(vec![
                         "**/.secretdir".to_string(),
                         "**/.mymetadata".to_string(),
                     ]);
-                    settings.private_files = Some(vec![
+                    settings.project.worktree.private_files = Some(vec![
                         "**/.mysecrets".to_string(),
                         "**/*.privatekey".to_string(),
                         "**/*.mysensitive".to_string(),
@@ -802,10 +801,10 @@ mod test {
         // Set global settings
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions =
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
                 });
             });
         });

crates/agent_servers/src/claude.rs πŸ”—

@@ -45,8 +45,13 @@ impl AgentServer for ClaudeCode {
     }
 
     fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
-        update_settings_file::<AllAgentServersSettings>(fs, cx, |settings, _| {
-            settings.claude.get_or_insert_default().default_mode = mode_id.map(|m| m.to_string())
+        update_settings_file(fs, cx, |settings, _| {
+            settings
+                .agent_servers
+                .get_or_insert_default()
+                .claude
+                .get_or_insert_default()
+                .default_mode = mode_id.map(|m| m.to_string())
         });
     }
 

crates/agent_servers/src/custom.rs πŸ”—

@@ -49,8 +49,14 @@ impl crate::AgentServer for CustomAgentServer {
 
     fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
         let name = self.name();
-        update_settings_file::<AllAgentServersSettings>(fs, cx, move |settings, _| {
-            settings.custom.get_mut(&name).unwrap().default_mode = mode_id.map(|m| m.to_string())
+        update_settings_file(fs, cx, move |settings, _| {
+            settings
+                .agent_servers
+                .get_or_insert_default()
+                .custom
+                .get_mut(&name)
+                .unwrap()
+                .default_mode = mode_id.map(|m| m.to_string())
         });
     }
 

crates/agent_settings/src/agent_profile.rs πŸ”—

@@ -1,15 +1,17 @@
 use std::sync::Arc;
 
+use anyhow::{Result, bail};
 use collections::IndexMap;
 use convert_case::{Case, Casing as _};
 use fs::Fs;
 use gpui::{App, SharedString};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings as _, update_settings_file};
+use settings::{
+    AgentProfileContent, ContextServerPresetContent, Settings as _, SettingsContent,
+    update_settings_file,
+};
 use util::ResultExt as _;
 
-use crate::AgentSettings;
+use crate::{AgentProfileId, AgentSettings};
 
 pub mod builtin_profiles {
     use super::AgentProfileId;
@@ -23,27 +25,6 @@ pub mod builtin_profiles {
     }
 }
 
-#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
-pub struct AgentProfileId(pub Arc<str>);
-
-impl AgentProfileId {
-    pub fn as_str(&self) -> &str {
-        &self.0
-    }
-}
-
-impl std::fmt::Display for AgentProfileId {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
-
-impl Default for AgentProfileId {
-    fn default() -> Self {
-        Self("write".into())
-    }
-}
-
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct AgentProfile {
     id: AgentProfileId,
@@ -87,10 +68,10 @@ impl AgentProfile {
                 .unwrap_or_default(),
         };
 
-        update_settings_file::<AgentSettings>(fs, cx, {
+        update_settings_file(fs, cx, {
             let id = id.clone();
             move |settings, _cx| {
-                settings.create_profile(id, profile_settings).log_err();
+                profile_settings.save_to_settings(id, settings).log_err();
             }
         });
 
@@ -129,9 +110,71 @@ impl AgentProfileSettings {
                 .get(server_id)
                 .is_some_and(|preset| preset.tools.get(tool_name) == Some(&true))
     }
+
+    pub fn save_to_settings(
+        &self,
+        profile_id: AgentProfileId,
+        content: &mut SettingsContent,
+    ) -> Result<()> {
+        let profiles = content
+            .agent
+            .get_or_insert_default()
+            .profiles
+            .get_or_insert_default();
+        if profiles.contains_key(&profile_id.0) {
+            bail!("profile with ID '{profile_id}' already exists");
+        }
+
+        profiles.insert(
+            profile_id.0,
+            AgentProfileContent {
+                name: self.name.clone().into(),
+                tools: self.tools.clone(),
+                enable_all_context_servers: Some(self.enable_all_context_servers),
+                context_servers: self
+                    .context_servers
+                    .clone()
+                    .into_iter()
+                    .map(|(server_id, preset)| {
+                        (
+                            server_id,
+                            ContextServerPresetContent {
+                                tools: preset.tools,
+                            },
+                        )
+                    })
+                    .collect(),
+            },
+        );
+
+        Ok(())
+    }
+}
+
+impl From<AgentProfileContent> for AgentProfileSettings {
+    fn from(content: AgentProfileContent) -> Self {
+        Self {
+            name: content.name.into(),
+            tools: content.tools,
+            enable_all_context_servers: content.enable_all_context_servers.unwrap_or_default(),
+            context_servers: content
+                .context_servers
+                .into_iter()
+                .map(|(server_id, preset)| (server_id, preset.into()))
+                .collect(),
+        }
+    }
 }
 
 #[derive(Debug, Clone, Default)]
 pub struct ContextServerPreset {
     pub tools: IndexMap<Arc<str>, bool>,
 }
+
+impl From<settings::ContextServerPresetContent> for ContextServerPreset {
+    fn from(content: settings::ContextServerPresetContent) -> Self {
+        Self {
+            tools: content.tools,
+        }
+    }
+}

crates/agent_settings/src/agent_settings.rs πŸ”—

@@ -2,14 +2,16 @@ mod agent_profile;
 
 use std::sync::Arc;
 
-use anyhow::{Result, bail};
 use collections::IndexMap;
-use gpui::{App, Pixels, SharedString};
+use gpui::{App, Pixels, px};
 use language_model::LanguageModel;
-use schemars::{JsonSchema, json_schema};
+use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
-use std::borrow::Cow;
+use settings::{
+    DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
+    NotifyWhenAgentWaiting, Settings, SettingsContent,
+};
+use util::MergeFrom;
 
 pub use crate::agent_profile::*;
 
@@ -22,37 +24,11 @@ pub fn init(cx: &mut App) {
     AgentSettings::register(cx);
 }
 
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum AgentDockPosition {
-    Left,
-    #[default]
-    Right,
-    Bottom,
-}
-
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum DefaultView {
-    #[default]
-    Thread,
-    TextThread,
-}
-
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum NotifyWhenAgentWaiting {
-    #[default]
-    PrimaryScreen,
-    AllScreens,
-    Never,
-}
-
-#[derive(Default, Clone, Debug)]
+#[derive(Clone, Debug)]
 pub struct AgentSettings {
     pub enabled: bool,
     pub button: bool,
-    pub dock: AgentDockPosition,
+    pub dock: DockPosition,
     pub default_width: Pixels,
     pub default_height: Pixels,
     pub default_model: Option<LanguageModelSelection>,
@@ -60,9 +36,8 @@ pub struct AgentSettings {
     pub commit_message_model: Option<LanguageModelSelection>,
     pub thread_summary_model: Option<LanguageModelSelection>,
     pub inline_alternatives: Vec<LanguageModelSelection>,
-    pub using_outdated_settings_version: bool,
     pub default_profile: AgentProfileId,
-    pub default_view: DefaultView,
+    pub default_view: DefaultAgentView,
     pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
     pub always_allow_tool_actions: bool,
     pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
@@ -81,11 +56,20 @@ pub struct AgentSettings {
 impl AgentSettings {
     pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
         let settings = Self::get_global(cx);
-        settings
-            .model_parameters
-            .iter()
-            .rfind(|setting| setting.matches(model))
-            .and_then(|m| m.temperature)
+        for setting in settings.model_parameters.iter().rev() {
+            if let Some(provider) = &setting.provider
+                && provider.0 != model.provider_id().0
+            {
+                continue;
+            }
+            if let Some(setting_model) = &setting.model
+                && *setting_model != model.id().0
+            {
+                continue;
+            }
+            return setting.temperature;
+        }
+        return None;
     }
 
     pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
@@ -114,223 +98,6 @@ impl AgentSettings {
     }
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
-pub struct LanguageModelParameters {
-    pub provider: Option<LanguageModelProviderSetting>,
-    pub model: Option<SharedString>,
-    pub temperature: Option<f32>,
-}
-
-impl LanguageModelParameters {
-    pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool {
-        if let Some(provider) = &self.provider
-            && provider.0 != model.provider_id().0
-        {
-            return false;
-        }
-        if let Some(setting_model) = &self.model
-            && *setting_model != model.id().0
-        {
-            return false;
-        }
-        true
-    }
-}
-
-impl AgentSettingsContent {
-    pub fn set_dock(&mut self, dock: AgentDockPosition) {
-        self.dock = Some(dock);
-    }
-
-    pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
-        let model = language_model.id().0.to_string();
-        let provider = language_model.provider_id().0.to_string();
-
-        self.default_model = Some(LanguageModelSelection {
-            provider: provider.into(),
-            model,
-        });
-    }
-
-    pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
-        self.inline_assistant_model = Some(LanguageModelSelection {
-            provider: provider.into(),
-            model,
-        });
-    }
-
-    pub fn set_commit_message_model(&mut self, provider: String, model: String) {
-        self.commit_message_model = Some(LanguageModelSelection {
-            provider: provider.into(),
-            model,
-        });
-    }
-
-    pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
-        self.thread_summary_model = Some(LanguageModelSelection {
-            provider: provider.into(),
-            model,
-        });
-    }
-
-    pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
-        self.always_allow_tool_actions = Some(allow);
-    }
-
-    pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
-        self.play_sound_when_agent_done = Some(allow);
-    }
-
-    pub fn set_single_file_review(&mut self, allow: bool) {
-        self.single_file_review = Some(allow);
-    }
-
-    pub fn set_use_modifier_to_send(&mut self, always_use: bool) {
-        self.use_modifier_to_send = Some(always_use);
-    }
-
-    pub fn set_profile(&mut self, profile_id: AgentProfileId) {
-        self.default_profile = Some(profile_id);
-    }
-
-    pub fn create_profile(
-        &mut self,
-        profile_id: AgentProfileId,
-        profile_settings: AgentProfileSettings,
-    ) -> Result<()> {
-        let profiles = self.profiles.get_or_insert_default();
-        if profiles.contains_key(&profile_id) {
-            bail!("profile with ID '{profile_id}' already exists");
-        }
-
-        profiles.insert(
-            profile_id,
-            AgentProfileContent {
-                name: profile_settings.name.into(),
-                tools: profile_settings.tools,
-                enable_all_context_servers: Some(profile_settings.enable_all_context_servers),
-                context_servers: profile_settings
-                    .context_servers
-                    .into_iter()
-                    .map(|(server_id, preset)| {
-                        (
-                            server_id,
-                            ContextServerPresetContent {
-                                tools: preset.tools,
-                            },
-                        )
-                    })
-                    .collect(),
-            },
-        );
-
-        Ok(())
-    }
-}
-
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
-#[settings_key(key = "agent", fallback_key = "assistant")]
-pub struct AgentSettingsContent {
-    /// Whether the Agent is enabled.
-    ///
-    /// Default: true
-    enabled: Option<bool>,
-    /// Whether to show the agent panel button in the status bar.
-    ///
-    /// Default: true
-    button: Option<bool>,
-    /// Where to dock the agent panel.
-    ///
-    /// Default: right
-    dock: Option<AgentDockPosition>,
-    /// Default width in pixels when the agent panel is docked to the left or right.
-    ///
-    /// Default: 640
-    default_width: Option<f32>,
-    /// Default height in pixels when the agent panel is docked to the bottom.
-    ///
-    /// Default: 320
-    default_height: Option<f32>,
-    /// The default model to use when creating new chats and for other features when a specific model is not specified.
-    default_model: Option<LanguageModelSelection>,
-    /// Model to use for the inline assistant. Defaults to default_model when not specified.
-    inline_assistant_model: Option<LanguageModelSelection>,
-    /// Model to use for generating git commit messages. Defaults to default_model when not specified.
-    commit_message_model: Option<LanguageModelSelection>,
-    /// Model to use for generating thread summaries. Defaults to default_model when not specified.
-    thread_summary_model: Option<LanguageModelSelection>,
-    /// Additional models with which to generate alternatives when performing inline assists.
-    inline_alternatives: Option<Vec<LanguageModelSelection>>,
-    /// The default profile to use in the Agent.
-    ///
-    /// Default: write
-    default_profile: Option<AgentProfileId>,
-    /// Which view type to show by default in the agent panel.
-    ///
-    /// Default: "thread"
-    default_view: Option<DefaultView>,
-    /// The available agent profiles.
-    pub profiles: Option<IndexMap<AgentProfileId, AgentProfileContent>>,
-    /// Whenever a tool action would normally wait for your confirmation
-    /// that you allow it, always choose to allow it.
-    ///
-    /// This setting has no effect on external agents that support permission modes, such as Claude Code.
-    ///
-    /// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code.
-    ///
-    /// Default: false
-    always_allow_tool_actions: Option<bool>,
-    /// Where to show a popup notification when the agent is waiting for user input.
-    ///
-    /// Default: "primary_screen"
-    notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
-    /// Whether to play a sound when the agent has either completed its response, or needs user input.
-    ///
-    /// Default: false
-    play_sound_when_agent_done: Option<bool>,
-    /// Whether to stream edits from the agent as they are received.
-    ///
-    /// Default: false
-    stream_edits: Option<bool>,
-    /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
-    ///
-    /// Default: true
-    single_file_review: Option<bool>,
-    /// Additional parameters for language model requests. When making a request
-    /// to a model, parameters will be taken from the last entry in this list
-    /// that matches the model's provider and name. In each entry, both provider
-    /// and model are optional, so that you can specify parameters for either
-    /// one.
-    ///
-    /// Default: []
-    #[serde(default)]
-    model_parameters: Vec<LanguageModelParameters>,
-    /// What completion mode to enable for new threads
-    ///
-    /// Default: normal
-    preferred_completion_mode: Option<CompletionMode>,
-    /// Whether to show thumb buttons for feedback in the agent panel.
-    ///
-    /// Default: true
-    enable_feedback: Option<bool>,
-    /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
-    ///
-    /// Default: true
-    expand_edit_card: Option<bool>,
-    /// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
-    ///
-    /// Default: true
-    expand_terminal_card: Option<bool>,
-    /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
-    ///
-    /// Default: false
-    use_modifier_to_send: Option<bool>,
-    /// Minimum number of lines of height the agent message editor should have.
-    ///
-    /// Default: 4
-    message_editor_min_lines: Option<usize>,
-}
-
 #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
 #[serde(rename_all = "snake_case")]
 pub enum CompletionMode {
@@ -349,215 +116,140 @@ impl From<CompletionMode> for cloud_llm_client::CompletionMode {
     }
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
-pub struct LanguageModelSelection {
-    pub provider: LanguageModelProviderSetting,
-    pub model: String,
+impl From<settings::CompletionMode> for CompletionMode {
+    fn from(value: settings::CompletionMode) -> Self {
+        match value {
+            settings::CompletionMode::Normal => CompletionMode::Normal,
+            settings::CompletionMode::Burn => CompletionMode::Burn,
+        }
+    }
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
-pub struct LanguageModelProviderSetting(pub String);
+#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct AgentProfileId(pub Arc<str>);
 
-impl JsonSchema for LanguageModelProviderSetting {
-    fn schema_name() -> Cow<'static, str> {
-        "LanguageModelProviderSetting".into()
-    }
-
-    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
-        // list the builtin providers as a subset so that we still auto complete them in the settings
-        json_schema!({
-            "anyOf": [
-                {
-                    "type": "string",
-                    "enum": [
-                        "amazon-bedrock",
-                        "anthropic",
-                        "copilot_chat",
-                        "deepseek",
-                        "google",
-                        "lmstudio",
-                        "mistral",
-                        "ollama",
-                        "openai",
-                        "openrouter",
-                        "vercel",
-                        "x_ai",
-                        "zed.dev"
-                    ]
-                },
-                {
-                    "type": "string",
-                }
-            ]
-        })
+impl AgentProfileId {
+    pub fn as_str(&self) -> &str {
+        &self.0
     }
 }
 
-impl From<String> for LanguageModelProviderSetting {
-    fn from(provider: String) -> Self {
-        Self(provider)
+impl std::fmt::Display for AgentProfileId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0)
     }
 }
 
-impl From<&str> for LanguageModelProviderSetting {
-    fn from(provider: &str) -> Self {
-        Self(provider.to_string())
+impl Default for AgentProfileId {
+    fn default() -> Self {
+        Self("write".into())
     }
 }
 
-#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
-pub struct AgentProfileContent {
-    pub name: Arc<str>,
-    #[serde(default)]
-    pub tools: IndexMap<Arc<str>, bool>,
-    /// Whether all context servers are enabled by default.
-    pub enable_all_context_servers: Option<bool>,
-    #[serde(default)]
-    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
-}
-
-#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
-pub struct ContextServerPresetContent {
-    pub tools: IndexMap<Arc<str>, bool>,
-}
-
 impl Settings for AgentSettings {
-    const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
-
-    type FileContent = AgentSettingsContent;
-
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _: &mut gpui::App,
-    ) -> anyhow::Result<Self> {
-        let mut settings = AgentSettings::default();
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let agent = content.agent.clone().unwrap();
+        Self {
+            enabled: agent.enabled.unwrap(),
+            button: agent.button.unwrap(),
+            dock: agent.dock.unwrap(),
+            default_width: px(agent.default_width.unwrap()),
+            default_height: px(agent.default_height.unwrap()),
+            default_model: Some(agent.default_model.unwrap()),
+            inline_assistant_model: agent.inline_assistant_model,
+            commit_message_model: agent.commit_message_model,
+            thread_summary_model: agent.thread_summary_model,
+            inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
+            default_profile: AgentProfileId(agent.default_profile.unwrap()),
+            default_view: agent.default_view.unwrap(),
+            profiles: agent
+                .profiles
+                .unwrap()
+                .into_iter()
+                .map(|(key, val)| (AgentProfileId(key), val.into()))
+                .collect(),
+            always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(),
+            notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(),
+            play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(),
+            stream_edits: agent.stream_edits.unwrap(),
+            single_file_review: agent.single_file_review.unwrap(),
+            model_parameters: agent.model_parameters,
+            preferred_completion_mode: agent.preferred_completion_mode.unwrap().into(),
+            enable_feedback: agent.enable_feedback.unwrap(),
+            expand_edit_card: agent.expand_edit_card.unwrap(),
+            expand_terminal_card: agent.expand_terminal_card.unwrap(),
+            use_modifier_to_send: agent.use_modifier_to_send.unwrap(),
+            message_editor_min_lines: agent.message_editor_min_lines.unwrap(),
+        }
+    }
 
-        for value in sources.defaults_and_customizations() {
-            merge(&mut settings.enabled, value.enabled);
-            merge(&mut settings.button, value.button);
-            merge(&mut settings.dock, value.dock);
-            merge(
-                &mut settings.default_width,
-                value.default_width.map(Into::into),
-            );
-            merge(
-                &mut settings.default_height,
-                value.default_height.map(Into::into),
-            );
-            settings.default_model = value
-                .default_model
-                .clone()
-                .or(settings.default_model.take());
-            settings.inline_assistant_model = value
-                .inline_assistant_model
-                .clone()
-                .or(settings.inline_assistant_model.take());
-            settings.commit_message_model = value
-                .clone()
-                .commit_message_model
-                .or(settings.commit_message_model.take());
-            settings.thread_summary_model = value
-                .clone()
-                .thread_summary_model
-                .or(settings.thread_summary_model.take());
-            merge(
-                &mut settings.inline_alternatives,
-                value.inline_alternatives.clone(),
-            );
-            merge(
-                &mut settings.notify_when_agent_waiting,
-                value.notify_when_agent_waiting,
-            );
-            merge(
-                &mut settings.play_sound_when_agent_done,
-                value.play_sound_when_agent_done,
-            );
-            merge(&mut settings.stream_edits, value.stream_edits);
-            merge(&mut settings.single_file_review, value.single_file_review);
-            merge(&mut settings.default_profile, value.default_profile.clone());
-            merge(&mut settings.default_view, value.default_view);
-            merge(
-                &mut settings.preferred_completion_mode,
-                value.preferred_completion_mode,
-            );
-            merge(&mut settings.enable_feedback, value.enable_feedback);
-            merge(&mut settings.expand_edit_card, value.expand_edit_card);
-            merge(
-                &mut settings.expand_terminal_card,
-                value.expand_terminal_card,
-            );
-            merge(
-                &mut settings.use_modifier_to_send,
-                value.use_modifier_to_send,
-            );
-            merge(
-                &mut settings.message_editor_min_lines,
-                value.message_editor_min_lines,
+    fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
+        let Some(value) = &content.agent else { return };
+        self.enabled.merge_from(&value.enabled);
+        self.button.merge_from(&value.button);
+        self.dock.merge_from(&value.dock);
+        self.default_width
+            .merge_from(&value.default_width.map(Into::into));
+        self.default_height
+            .merge_from(&value.default_height.map(Into::into));
+        self.default_model = value.default_model.clone().or(self.default_model.take());
+
+        self.inline_assistant_model = value
+            .inline_assistant_model
+            .clone()
+            .or(self.inline_assistant_model.take());
+        self.commit_message_model = value
+            .clone()
+            .commit_message_model
+            .or(self.commit_message_model.take());
+        self.thread_summary_model = value
+            .clone()
+            .thread_summary_model
+            .or(self.thread_summary_model.take());
+        self.inline_alternatives
+            .merge_from(&value.inline_alternatives.clone());
+        self.default_profile
+            .merge_from(&value.default_profile.clone().map(AgentProfileId));
+        self.default_view.merge_from(&value.default_view);
+        self.always_allow_tool_actions
+            .merge_from(&value.always_allow_tool_actions);
+        self.notify_when_agent_waiting
+            .merge_from(&value.notify_when_agent_waiting);
+        self.play_sound_when_agent_done
+            .merge_from(&value.play_sound_when_agent_done);
+        self.stream_edits.merge_from(&value.stream_edits);
+        self.single_file_review
+            .merge_from(&value.single_file_review);
+        self.preferred_completion_mode
+            .merge_from(&value.preferred_completion_mode.map(Into::into));
+        self.enable_feedback.merge_from(&value.enable_feedback);
+        self.expand_edit_card.merge_from(&value.expand_edit_card);
+        self.expand_terminal_card
+            .merge_from(&value.expand_terminal_card);
+        self.use_modifier_to_send
+            .merge_from(&value.use_modifier_to_send);
+
+        self.model_parameters
+            .extend_from_slice(&value.model_parameters);
+        self.message_editor_min_lines
+            .merge_from(&value.message_editor_min_lines);
+
+        if let Some(profiles) = value.profiles.as_ref() {
+            self.profiles.extend(
+                profiles
+                    .into_iter()
+                    .map(|(id, profile)| (AgentProfileId(id.clone()), profile.clone().into())),
             );
-
-            settings
-                .model_parameters
-                .extend_from_slice(&value.model_parameters);
-
-            if let Some(profiles) = value.profiles.as_ref() {
-                settings
-                    .profiles
-                    .extend(profiles.into_iter().map(|(id, profile)| {
-                        (
-                            id.clone(),
-                            AgentProfileSettings {
-                                name: profile.name.clone().into(),
-                                tools: profile.tools.clone(),
-                                enable_all_context_servers: profile
-                                    .enable_all_context_servers
-                                    .unwrap_or_default(),
-                                context_servers: profile
-                                    .context_servers
-                                    .iter()
-                                    .map(|(context_server_id, preset)| {
-                                        (
-                                            context_server_id.clone(),
-                                            ContextServerPreset {
-                                                tools: preset.tools.clone(),
-                                            },
-                                        )
-                                    })
-                                    .collect(),
-                            },
-                        )
-                    }));
-            }
         }
-
-        debug_assert!(
-            !sources.default.always_allow_tool_actions.unwrap_or(false),
-            "For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!"
-        );
-
-        // For security reasons, only trust the user's global settings for whether to always allow tool actions.
-        // If this could be overridden locally, an attacker could (e.g. by committing to source control and
-        // convincing you to switch branches) modify your project-local settings to disable the agent's safety checks.
-        settings.always_allow_tool_actions = sources
-            .user
-            .and_then(|setting| setting.always_allow_tool_actions)
-            .unwrap_or(false);
-
-        Ok(settings)
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
+    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
         if let Some(b) = vscode
             .read_value("chat.agent.enabled")
             .and_then(|b| b.as_bool())
         {
-            current.enabled = Some(b);
-            current.button = Some(b);
+            current.agent.get_or_insert_default().enabled = Some(b);
+            current.agent.get_or_insert_default().button = Some(b);
         }
     }
 }
-
-fn merge<T>(target: &mut T, value: Option<T>) {
-    if let Some(value) = value {
-        *target = value;
-    }
-}

crates/agent_ui/src/acp/thread_view.rs πŸ”—

@@ -7,7 +7,7 @@ use acp_thread::{AgentConnection, Plan};
 use action_log::ActionLog;
 use agent_client_protocol::{self as acp, PromptCapabilities};
 use agent_servers::{AgentServer, AgentServerDelegate};
-use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
+use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
 use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
 use anyhow::{Result, anyhow, bail};
 use arrayvec::ArrayVec;
@@ -35,7 +35,7 @@ use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
 use project::{Project, ProjectEntryId};
 use prompt_store::{PromptId, PromptStore};
 use rope::Point;
-use settings::{Settings as _, SettingsStore};
+use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
 use std::cell::RefCell;
 use std::path::Path;
 use std::sync::Arc;

crates/agent_ui/src/agent_configuration.rs πŸ”—

@@ -26,12 +26,8 @@ use language_model::{
 };
 use notifications::status_toast::{StatusToast, ToastIcon};
 use project::{
-    agent_server_store::{
-        AgentServerCommand, AgentServerStore, AllAgentServersSettings, CLAUDE_CODE_NAME,
-        CustomAgentServerSettings, GEMINI_NAME,
-    },
+    agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, GEMINI_NAME},
     context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
-    project_settings::{ContextServerSettings, ProjectSettings},
 };
 use settings::{Settings, SettingsStore, update_settings_file};
 use ui::{
@@ -419,8 +415,8 @@ impl AgentConfiguration {
             always_allow_tool_actions,
             move |state, _window, cx| {
                 let allow = state == &ToggleState::Selected;
-                update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
-                    settings.set_always_allow_tool_actions(allow);
+                update_settings_file(fs.clone(), cx, move |settings, _| {
+                    settings.agent.get_or_insert_default().set_always_allow_tool_actions(allow);
                 });
             },
         )
@@ -437,8 +433,11 @@ impl AgentConfiguration {
             single_file_review,
             move |state, _window, cx| {
                 let allow = state == &ToggleState::Selected;
-                update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
-                    settings.set_single_file_review(allow);
+                update_settings_file(fs.clone(), cx, move |settings, _| {
+                    settings
+                        .agent
+                        .get_or_insert_default()
+                        .set_single_file_review(allow);
                 });
             },
         )
@@ -457,8 +456,8 @@ impl AgentConfiguration {
             play_sound_when_agent_done,
             move |state, _window, cx| {
                 let allow = state == &ToggleState::Selected;
-                update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
-                    settings.set_play_sound_when_agent_done(allow);
+                update_settings_file(fs.clone(), cx, move |settings, _| {
+                    settings.agent.get_or_insert_default().set_play_sound_when_agent_done(allow);
                 });
             },
         )
@@ -477,8 +476,8 @@ impl AgentConfiguration {
             use_modifier_to_send,
             move |state, _window, cx| {
                 let allow = state == &ToggleState::Selected;
-                update_settings_file::<AgentSettings>(fs.clone(), cx, move |settings, _| {
-                    settings.set_use_modifier_to_send(allow);
+                update_settings_file(fs.clone(), cx, move |settings, _| {
+                    settings.agent.get_or_insert_default().set_use_modifier_to_send(allow);
                 });
             },
         )
@@ -862,14 +861,14 @@ impl AgentConfiguration {
                                     async move |cx| {
                                         uninstall_extension_task.await?;
                                         cx.update(|cx| {
-                                            update_settings_file::<ProjectSettings>(
+                                            update_settings_file(
                                                 fs.clone(),
                                                 cx,
                                                 {
                                                     let context_server_id =
                                                         context_server_id.clone();
                                                     move |settings, _| {
-                                                        settings
+                                                        settings.project
                                                             .context_servers
                                                             .remove(&context_server_id.0);
                                                     }
@@ -944,67 +943,53 @@ impl AgentConfiguration {
                             .flex_none()
                             .child(context_server_configuration_menu)
                             .child(
-                                Switch::new("context-server-switch", is_running.into())
-                                    .color(SwitchColor::Accent)
-                                    .on_click({
-                                        let context_server_manager =
-                                            self.context_server_store.clone();
-                                        let fs = self.fs.clone();
-
-                                        move |state, _window, cx| {
-                                            let is_enabled = match state {
-                                                ToggleState::Unselected
-                                                | ToggleState::Indeterminate => {
-                                                    context_server_manager.update(
-                                                        cx,
-                                                        |this, cx| {
-                                                            this.stop_server(
-                                                                &context_server_id,
-                                                                cx,
-                                                            )
-                                                            .log_err();
-                                                        },
-                                                    );
-                                                    false
-                                                }
-                                                ToggleState::Selected => {
-                                                    context_server_manager.update(
-                                                        cx,
-                                                        |this, cx| {
-                                                            if let Some(server) =
-                                                                this.get_server(&context_server_id)
-                                                            {
-                                                                this.start_server(server, cx);
-                                                            }
-                                                        },
-                                                    );
-                                                    true
-                                                }
-                                            };
-                                            update_settings_file::<ProjectSettings>(
-                                                fs.clone(),
-                                                cx,
-                                                {
-                                                    let context_server_id =
-                                                        context_server_id.clone();
-
-                                                    move |settings, _| {
-                                                        settings
-                                                            .context_servers
-                                                            .entry(context_server_id.0)
-                                                            .or_insert_with(|| {
-                                                                ContextServerSettings::Extension {
-                                                                    enabled: is_enabled,
-                                                                    settings: serde_json::json!({}),
-                                                                }
-                                                            })
-                                                            .set_enabled(is_enabled);
+                            Switch::new("context-server-switch", is_running.into())
+                                .color(SwitchColor::Accent)
+                                .on_click({
+                                    let context_server_manager = self.context_server_store.clone();
+                                    let fs = self.fs.clone();
+
+                                    move |state, _window, cx| {
+                                        let is_enabled = match state {
+                                            ToggleState::Unselected
+                                            | ToggleState::Indeterminate => {
+                                                context_server_manager.update(cx, |this, cx| {
+                                                    this.stop_server(&context_server_id, cx)
+                                                        .log_err();
+                                                });
+                                                false
+                                            }
+                                            ToggleState::Selected => {
+                                                context_server_manager.update(cx, |this, cx| {
+                                                    if let Some(server) =
+                                                        this.get_server(&context_server_id)
+                                                    {
+                                                        this.start_server(server, cx);
                                                     }
-                                                },
-                                            );
-                                        }
-                                    }),
-                            ),
+                                                });
+                                                true
+                                            }
+                                        };
+                                        update_settings_file(fs.clone(), cx, {
+                                            let context_server_id = context_server_id.clone();
+
+                                            move |settings, _| {
+                                                settings
+                                                    .project
+                                                    .context_servers
+                                                    .entry(context_server_id.0)
+                                                    .or_insert_with(|| {
+                                                        settings::ContextServerSettingsContent::Extension {
+                                                            enabled: is_enabled,
+                                                            settings: serde_json::json!({}),
+                                                        }
+                                                    })
+                                                    .set_enabled(is_enabled);
+                                            }
+                                        });
+                                    }
+                                }),
+                        ),
                     ),
             )
             .map(|parent| {
@@ -1236,15 +1221,12 @@ fn show_unable_to_uninstall_extension_with_context_server(
                                     let context_server_id = context_server_id.clone();
                                     async move |_workspace_handle, cx| {
                                         cx.update(|cx| {
-                                            update_settings_file::<ProjectSettings>(
-                                                fs,
-                                                cx,
-                                                move |settings, _| {
-                                                    settings
-                                                        .context_servers
-                                                        .remove(&context_server_id.0);
-                                                },
-                                            );
+                                            update_settings_file(fs, cx, move |settings, _| {
+                                                settings
+                                                    .project
+                                                    .context_servers
+                                                    .remove(&context_server_id.0);
+                                            });
                                         })?;
                                         anyhow::Ok(())
                                     }
@@ -1282,7 +1264,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
             let settings = cx.global::<SettingsStore>();
 
             let mut unique_server_name = None;
-            let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
+            let edits = settings.edits_for_update(&text, |settings| {
                 let server_name: Option<SharedString> = (0..u8::MAX)
                     .map(|i| {
                         if i == 0 {
@@ -1291,20 +1273,27 @@ async fn open_new_agent_servers_entry_in_settings_editor(
                             format!("your_agent_{}", i).into()
                         }
                     })
-                    .find(|name| !file.custom.contains_key(name));
+                    .find(|name| {
+                        !settings
+                            .agent_servers
+                            .as_ref()
+                            .is_some_and(|agent_servers| agent_servers.custom.contains_key(name))
+                    });
                 if let Some(server_name) = server_name {
                     unique_server_name = Some(server_name.clone());
-                    file.custom.insert(
-                        server_name,
-                        CustomAgentServerSettings {
-                            command: AgentServerCommand {
+                    settings
+                        .agent_servers
+                        .get_or_insert_default()
+                        .custom
+                        .insert(
+                            server_name,
+                            settings::CustomAgentServerSettings {
                                 path: "path_to_executable".into(),
                                 args: vec![],
                                 env: Some(HashMap::default()),
+                                default_mode: None,
                             },
-                            default_mode: None,
-                        },
-                    );
+                        );
                 }
             });
 

crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs πŸ”—

@@ -5,11 +5,8 @@ use collections::HashSet;
 use fs::Fs;
 use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
 use language_model::LanguageModelRegistry;
-use language_models::{
-    AllLanguageModelSettings, OpenAiCompatibleSettingsContent,
-    provider::open_ai_compatible::{AvailableModel, ModelCapabilities},
-};
-use settings::update_settings_file;
+use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities};
+use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
 use ui::{
     Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
 };
@@ -238,14 +235,19 @@ fn save_provider_to_settings(
         task.await
             .map_err(|_| "Failed to write API key to keychain")?;
         cx.update(|cx| {
-            update_settings_file::<AllLanguageModelSettings>(fs, cx, |settings, _cx| {
-                settings.openai_compatible.get_or_insert_default().insert(
-                    provider_name,
-                    OpenAiCompatibleSettingsContent {
-                        api_url,
-                        available_models: models,
-                    },
-                );
+            update_settings_file(fs, cx, |settings, _cx| {
+                settings
+                    .language_models
+                    .get_or_insert_default()
+                    .openai_compatible
+                    .get_or_insert_default()
+                    .insert(
+                        provider_name,
+                        OpenAiCompatibleSettingsContent {
+                            api_url,
+                            available_models: models,
+                        },
+                    );
             });
         })
         .ok();

crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs πŸ”—

@@ -422,18 +422,17 @@ impl ConfigureContextServerModal {
             workspace.update(cx, |workspace, cx| {
                 let fs = workspace.app_state().fs.clone();
                 let original_server_id = self.original_server_id.clone();
-                update_settings_file::<ProjectSettings>(
-                    fs.clone(),
-                    cx,
-                    move |project_settings, _| {
-                        if let Some(original_id) = original_server_id {
-                            if original_id != id {
-                                project_settings.context_servers.remove(&original_id.0);
-                            }
+                update_settings_file(fs.clone(), cx, move |current, _| {
+                    if let Some(original_id) = original_server_id {
+                        if original_id != id {
+                            current.project.context_servers.remove(&original_id.0);
                         }
-                        project_settings.context_servers.insert(id.0, settings);
-                    },
-                );
+                    }
+                    current
+                        .project
+                        .context_servers
+                        .insert(id.0, settings.into());
+                });
             });
         } else if let Some(existing_server) = existing_server {
             self.context_server_store

crates/agent_ui/src/agent_configuration/tool_picker.rs πŸ”—

@@ -1,14 +1,11 @@
 use std::{collections::BTreeMap, sync::Arc};
 
-use agent_settings::{
-    AgentProfileContent, AgentProfileId, AgentProfileSettings, AgentSettings, AgentSettingsContent,
-    ContextServerPresetContent,
-};
+use agent_settings::{AgentProfileId, AgentProfileSettings};
 use assistant_tool::{ToolSource, ToolWorkingSet};
 use fs::Fs;
 use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
 use picker::{Picker, PickerDelegate};
-use settings::update_settings_file;
+use settings::{AgentProfileContent, ContextServerPresetContent, update_settings_file};
 use ui::{ListItem, ListItemSpacing, prelude::*};
 use util::ResultExt as _;
 
@@ -266,15 +263,19 @@ impl PickerDelegate for ToolPickerDelegate {
             is_enabled
         };
 
-        update_settings_file::<AgentSettings>(self.fs.clone(), cx, {
+        update_settings_file(self.fs.clone(), cx, {
             let profile_id = self.profile_id.clone();
             let default_profile = self.profile_settings.clone();
             let server_id = server_id.clone();
             let tool_name = tool_name.clone();
-            move |settings: &mut AgentSettingsContent, _cx| {
-                let profiles = settings.profiles.get_or_insert_default();
+            move |settings, _cx| {
+                let profiles = settings
+                    .agent
+                    .get_or_insert_default()
+                    .profiles
+                    .get_or_insert_default();
                 let profile = profiles
-                    .entry(profile_id)
+                    .entry(profile_id.0)
                     .or_insert_with(|| AgentProfileContent {
                         name: default_profile.name.into(),
                         tools: default_profile.tools,

crates/agent_ui/src/agent_model_selector.rs πŸ”—

@@ -2,7 +2,6 @@ use crate::{
     ModelUsageContext,
     language_model_selector::{LanguageModelSelector, language_model_selector},
 };
-use agent_settings::AgentSettings;
 use fs::Fs;
 use gpui::{Entity, FocusHandle, SharedString};
 use picker::popover_menu::PickerPopoverMenu;
@@ -39,14 +38,12 @@ impl AgentModelSelector {
                         let model_id = model.id().0.to_string();
                         match &model_usage_context {
                             ModelUsageContext::InlineAssistant => {
-                                update_settings_file::<AgentSettings>(
-                                    fs.clone(),
-                                    cx,
-                                    move |settings, _cx| {
-                                        settings
-                                            .set_inline_assistant_model(provider.clone(), model_id);
-                                    },
-                                );
+                                update_settings_file(fs.clone(), cx, move |settings, _cx| {
+                                    settings
+                                        .agent
+                                        .get_or_insert_default()
+                                        .set_inline_assistant_model(provider.clone(), model_id);
+                                });
                             }
                         }
                     },

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -10,6 +10,9 @@ use project::agent_server_store::{
     AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME,
 };
 use serde::{Deserialize, Serialize};
+use settings::{
+    DefaultAgentView as DefaultView, LanguageModelProviderSetting, LanguageModelSelection,
+};
 use zed_actions::OpenBrowser;
 use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
 
@@ -33,7 +36,7 @@ use agent::{
     history_store::{HistoryEntryId, HistoryStore},
     thread_store::{TextThreadStore, ThreadStore},
 };
-use agent_settings::{AgentDockPosition, AgentSettings, DefaultView};
+use agent_settings::AgentSettings;
 use ai_onboarding::AgentPanelOnboarding;
 use anyhow::{Result, anyhow};
 use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
@@ -1058,17 +1061,14 @@ impl AgentPanel {
         match self.active_view.which_font_size_used() {
             WhichFontSize::AgentFont => {
                 if persist {
-                    update_settings_file::<ThemeSettings>(
-                        self.fs.clone(),
-                        cx,
-                        move |settings, cx| {
-                            let agent_font_size =
-                                ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
-                            let _ = settings
-                                .agent_font_size
-                                .insert(Some(theme::clamp_font_size(agent_font_size).into()));
-                        },
-                    );
+                    update_settings_file(self.fs.clone(), cx, move |settings, cx| {
+                        let agent_font_size =
+                            ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
+                        let _ = settings
+                            .theme
+                            .agent_font_size
+                            .insert(Some(theme::clamp_font_size(agent_font_size).into()));
+                    });
                 } else {
                     theme::adjust_agent_font_size(cx, |size| size + delta);
                 }
@@ -1089,8 +1089,8 @@ impl AgentPanel {
         cx: &mut Context<Self>,
     ) {
         if action.persist {
-            update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
-                settings.agent_font_size = None;
+            update_settings_file(self.fs.clone(), cx, move |settings, _| {
+                settings.theme.agent_font_size = None;
             });
         } else {
             theme::reset_agent_font_size(cx);
@@ -1175,11 +1175,17 @@ impl AgentPanel {
                     .is_none_or(|model| model.provider.id() != provider.id())
                     && let Some(model) = provider.default_model(cx)
                 {
-                    update_settings_file::<AgentSettings>(
-                        self.fs.clone(),
-                        cx,
-                        move |settings, _| settings.set_model(model),
-                    );
+                    update_settings_file(self.fs.clone(), cx, move |settings, _| {
+                        let provider = model.provider_id().0.to_string();
+                        let model = model.id().0.to_string();
+                        settings
+                            .agent
+                            .get_or_insert_default()
+                            .set_model(LanguageModelSelection {
+                                provider: LanguageModelProviderSetting(provider),
+                                model,
+                            })
+                    });
                 }
 
                 self.new_thread(&NewThread::default(), window, cx);
@@ -1424,11 +1430,7 @@ impl Focusable for AgentPanel {
 }
 
 fn agent_panel_dock_position(cx: &App) -> DockPosition {
-    match AgentSettings::get_global(cx).dock {
-        AgentDockPosition::Left => DockPosition::Left,
-        AgentDockPosition::Bottom => DockPosition::Bottom,
-        AgentDockPosition::Right => DockPosition::Right,
-    }
+    AgentSettings::get_global(cx).dock.into()
 }
 
 impl EventEmitter<PanelEvent> for AgentPanel {}
@@ -1447,13 +1449,11 @@ impl Panel for AgentPanel {
     }
 
     fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
-        settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
-            let dock = match position {
-                DockPosition::Left => AgentDockPosition::Left,
-                DockPosition::Bottom => AgentDockPosition::Bottom,
-                DockPosition::Right => AgentDockPosition::Right,
-            };
-            settings.set_dock(dock);
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            settings
+                .agent
+                .get_or_insert_default()
+                .set_dock(position.into());
         });
     }
 

crates/agent_ui/src/agent_ui.rs πŸ”—

@@ -23,7 +23,7 @@ use std::rc::Rc;
 use std::sync::Arc;
 
 use agent::ThreadId;
-use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
+use agent_settings::{AgentProfileId, AgentSettings};
 use assistant_slash_command::SlashCommandRegistry;
 use client::Client;
 use command_palette_hooks::CommandPaletteFilter;
@@ -39,7 +39,7 @@ use project::agent_server_store::AgentServerCommand;
 use prompt_store::PromptBuilder;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings as _, SettingsStore};
+use settings::{LanguageModelSelection, Settings as _, SettingsStore};
 use std::any::TypeId;
 
 use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};

crates/agent_ui/src/context_server_configuration.rs πŸ”—

@@ -5,7 +5,6 @@ use extension::ExtensionManifest;
 use fs::Fs;
 use gpui::WeakEntity;
 use language::LanguageRegistry;
-use project::project_settings::ProjectSettings;
 use settings::update_settings_file;
 use ui::prelude::*;
 use util::ResultExt;
@@ -69,8 +68,9 @@ fn remove_context_server_settings(
     fs: Arc<dyn Fs>,
     cx: &mut App,
 ) {
-    update_settings_file::<ProjectSettings>(fs, cx, move |settings, _| {
+    update_settings_file(fs, cx, move |settings, _| {
         settings
+            .project
             .context_servers
             .retain(|server_id, _| !context_server_ids.contains(server_id));
     });

crates/agent_ui/src/profile_selector.rs πŸ”—

@@ -1,11 +1,10 @@
 use crate::{ManageProfiles, ToggleProfileSelector};
 use agent_settings::{
-    AgentDockPosition, AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles,
-    builtin_profiles,
+    AgentProfile, AgentProfileId, AgentSettings, AvailableProfiles, builtin_profiles,
 };
 use fs::Fs;
 use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*};
-use settings::{Settings as _, SettingsStore, update_settings_file};
+use settings::{DockPosition, Settings as _, SettingsStore, update_settings_file};
 use std::sync::Arc;
 use ui::{
     ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, PopoverMenu,
@@ -142,10 +141,13 @@ impl ProfileSelector {
             let fs = self.fs.clone();
             let provider = self.provider.clone();
             move |_window, cx| {
-                update_settings_file::<AgentSettings>(fs.clone(), cx, {
+                update_settings_file(fs.clone(), cx, {
                     let profile_id = profile_id.clone();
                     move |settings, _cx| {
-                        settings.set_profile(profile_id);
+                        settings
+                            .agent
+                            .get_or_insert_default()
+                            .set_profile(profile_id.0);
                     }
                 });
 
@@ -216,10 +218,10 @@ impl Render for ProfileSelector {
     }
 }
 
-fn documentation_side(position: AgentDockPosition) -> DocumentationSide {
+fn documentation_side(position: DockPosition) -> DocumentationSide {
     match position {
-        AgentDockPosition::Left => DocumentationSide::Right,
-        AgentDockPosition::Bottom => DocumentationSide::Left,
-        AgentDockPosition::Right => DocumentationSide::Left,
+        DockPosition::Left => DocumentationSide::Right,
+        DockPosition::Bottom => DocumentationSide::Left,
+        DockPosition::Right => DocumentationSide::Left,
     }
 }

crates/agent_ui/src/text_thread_editor.rs πŸ”—

@@ -3,7 +3,7 @@ use crate::{
     language_model_selector::{LanguageModelSelector, language_model_selector},
     ui::BurnModeTooltip,
 };
-use agent_settings::{AgentSettings, CompletionMode};
+use agent_settings::CompletionMode;
 use anyhow::Result;
 use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
 use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
@@ -41,7 +41,10 @@ use project::{Project, Worktree};
 use project::{ProjectPath, lsp_store::LocalLspAdapterDelegate};
 use rope::Point;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore, update_settings_file};
+use settings::{
+    LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore,
+    update_settings_file,
+};
 use std::{
     any::TypeId,
     cmp,
@@ -294,11 +297,16 @@ impl TextThreadEditor {
                 language_model_selector(
                     |cx| LanguageModelRegistry::read_global(cx).default_model(),
                     move |model, cx| {
-                        update_settings_file::<AgentSettings>(
-                            fs.clone(),
-                            cx,
-                            move |settings, _| settings.set_model(model.clone()),
-                        );
+                        update_settings_file(fs.clone(), cx, move |settings, _| {
+                            let provider = model.provider_id().0.to_string();
+                            let model = model.id().0.to_string();
+                            settings.agent.get_or_insert_default().set_model(
+                                LanguageModelSelection {
+                                    provider: LanguageModelProviderSetting(provider),
+                                    model,
+                                },
+                            )
+                        });
                     },
                     window,
                     cx,

crates/anthropic/Cargo.toml πŸ”—

@@ -23,6 +23,7 @@ http_client.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
+settings.workspace = true
 strum.workspace = true
 thiserror.workspace = true
 workspace-hack.workspace = true

crates/anthropic/src/anthropic.rs πŸ”—

@@ -8,6 +8,7 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B
 use http_client::http::{self, HeaderMap, HeaderValue};
 use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode};
 use serde::{Deserialize, Serialize};
+pub use settings::{AnthropicAvailableModel as AvailableModel, ModelMode};
 use strum::{EnumIter, EnumString};
 use thiserror::Error;
 
@@ -31,6 +32,24 @@ pub enum AnthropicModelMode {
     },
 }
 
+impl From<ModelMode> for AnthropicModelMode {
+    fn from(value: ModelMode) -> Self {
+        match value {
+            ModelMode::Default => AnthropicModelMode::Default,
+            ModelMode::Thinking { budget_tokens } => AnthropicModelMode::Thinking { budget_tokens },
+        }
+    }
+}
+
+impl From<AnthropicModelMode> for ModelMode {
+    fn from(value: AnthropicModelMode) -> Self {
+        match value {
+            AnthropicModelMode::Default => ModelMode::Default,
+            AnthropicModelMode::Thinking { budget_tokens } => ModelMode::Thinking { budget_tokens },
+        }
+    }
+}
+
 #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
 pub enum Model {

crates/assistant_tools/src/edit_file_tool.rs πŸ”—

@@ -1445,8 +1445,8 @@ mod tests {
 
     fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) {
         cx.update(|cx| {
-            // Set custom data directory (config will be under data_dir/config)
             paths::set_custom_data_dir(data_dir.to_str().unwrap());
+            // Set custom data directory (config will be under data_dir/config)
 
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
@@ -1537,14 +1537,11 @@ mod tests {
         // First, test with format_on_save enabled
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.format_on_save = Some(FormatOnSave::On);
-                        settings.defaults.formatter =
-                            Some(language::language_settings::SelectedFormatter::Auto);
-                    },
-                );
+                store.update_user_settings(cx, |settings| {
+                    settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
+                    settings.project.all_languages.defaults.formatter =
+                        Some(language::language_settings::SelectedFormatter::Auto);
+                });
             });
         });
 
@@ -1603,12 +1600,10 @@ mod tests {
         // Next, test with format_on_save disabled
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.format_on_save = Some(FormatOnSave::Off);
-                    },
-                );
+                store.update_user_settings(cx, |settings| {
+                    settings.project.all_languages.defaults.format_on_save =
+                        Some(FormatOnSave::Off);
+                });
             });
         });
 
@@ -1679,12 +1674,13 @@ mod tests {
         // First, test with remove_trailing_whitespace_on_save enabled
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.remove_trailing_whitespace_on_save = Some(true);
-                    },
-                );
+                store.update_user_settings(cx, |settings| {
+                    settings
+                        .project
+                        .all_languages
+                        .defaults
+                        .remove_trailing_whitespace_on_save = Some(true);
+                });
             });
         });
 
@@ -1741,12 +1737,13 @@ mod tests {
         // Next, test with remove_trailing_whitespace_on_save disabled
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<language::language_settings::AllLanguageSettings>(
-                    cx,
-                    |settings| {
-                        settings.defaults.remove_trailing_whitespace_on_save = Some(false);
-                    },
-                );
+                store.update_user_settings(cx, |settings| {
+                    settings
+                        .project
+                        .all_languages
+                        .defaults
+                        .remove_trailing_whitespace_on_save = Some(false);
+                });
             });
         });
 

crates/assistant_tools/src/grep_tool.rs πŸ”—

@@ -314,7 +314,7 @@ mod tests {
     use gpui::{AppContext, TestAppContext, UpdateGlobal};
     use language::{Language, LanguageConfig, LanguageMatcher};
     use language_model::fake_provider::FakeLanguageModel;
-    use project::{FakeFs, Project, WorktreeSettings};
+    use project::{FakeFs, Project};
     use serde_json::json;
     use settings::SettingsStore;
     use unindent::Unindent;
@@ -849,15 +849,14 @@ mod tests {
 
         cx.update(|cx| {
             use gpui::UpdateGlobal;
-            use project::WorktreeSettings;
             use settings::SettingsStore;
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions = Some(vec![
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions = Some(vec![
                         "**/.secretdir".to_string(),
                         "**/.mymetadata".to_string(),
                     ]);
-                    settings.private_files = Some(vec![
+                    settings.project.worktree.private_files = Some(vec![
                         "**/.mysecrets".to_string(),
                         "**/*.privatekey".to_string(),
                         "**/*.mysensitive".to_string(),
@@ -1158,10 +1157,10 @@ mod tests {
         // Set global settings
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions =
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
                 });
             });
         });

crates/assistant_tools/src/list_directory_tool.rs πŸ”—

@@ -230,7 +230,7 @@ mod tests {
     use gpui::{AppContext, TestAppContext, UpdateGlobal};
     use indoc::indoc;
     use language_model::fake_provider::FakeLanguageModel;
-    use project::{FakeFs, Project, WorktreeSettings};
+    use project::{FakeFs, Project};
     use serde_json::json;
     use settings::SettingsStore;
     use util::path;
@@ -507,13 +507,13 @@ mod tests {
         // Configure settings explicitly
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions = Some(vec![
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions = Some(vec![
                         "**/.secretdir".to_string(),
                         "**/.mymetadata".to_string(),
                         "**/.hidden_subdir".to_string(),
                     ]);
-                    settings.private_files = Some(vec![
+                    settings.project.worktree.private_files = Some(vec![
                         "**/.mysecrets".to_string(),
                         "**/*.privatekey".to_string(),
                         "**/*.mysensitive".to_string(),
@@ -698,10 +698,10 @@ mod tests {
         // Set global settings
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions =
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
                 });
             });
         });

crates/assistant_tools/src/read_file_tool.rs πŸ”—

@@ -299,7 +299,7 @@ mod test {
     use gpui::{AppContext, TestAppContext, UpdateGlobal};
     use language::{Language, LanguageConfig, LanguageMatcher};
     use language_model::fake_provider::FakeLanguageModel;
-    use project::{FakeFs, Project, WorktreeSettings};
+    use project::{FakeFs, Project};
     use serde_json::json;
     use settings::SettingsStore;
     use util::path;
@@ -677,15 +677,14 @@ mod test {
 
         cx.update(|cx| {
             use gpui::UpdateGlobal;
-            use project::WorktreeSettings;
             use settings::SettingsStore;
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions = Some(vec![
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions = Some(vec![
                         "**/.secretdir".to_string(),
                         "**/.mymetadata".to_string(),
                     ]);
-                    settings.private_files = Some(vec![
+                    settings.project.worktree.private_files = Some(vec![
                         "**/.mysecrets".to_string(),
                         "**/*.privatekey".to_string(),
                         "**/*.mysensitive".to_string(),
@@ -968,10 +967,10 @@ mod test {
         // Set global settings
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions =
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions =
                         Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
-                    settings.private_files = Some(vec!["**/.env".to_string()]);
+                    settings.project.worktree.private_files = Some(vec!["**/.env".to_string()]);
                 });
             });
         });

crates/audio/Cargo.toml πŸ”—

@@ -21,7 +21,6 @@ gpui.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }
-schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
 smol.workspace = true

crates/audio/src/audio_settings.rs πŸ”—

@@ -1,62 +1,53 @@
 use std::sync::atomic::{AtomicBool, Ordering};
 
-use anyhow::Result;
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
+use settings::{Settings, SettingsStore};
+use util::MergeFrom as _;
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+#[derive(Clone, Debug)]
 pub struct AudioSettings {
     /// Opt into the new audio system.
-    #[serde(rename = "experimental.rodio_audio", default)]
     pub rodio_audio: bool, // default is false
     /// Requires 'rodio_audio: true'
     ///
     /// Use the new audio systems automatic gain control for your microphone.
     /// This affects how loud you sound to others.
-    #[serde(rename = "experimental.control_input_volume", default)]
     pub control_input_volume: bool,
     /// Requires 'rodio_audio: true'
     ///
     /// Use the new audio systems automatic gain control on everyone in the
     /// call. This makes call members who are too quite louder and those who are
     /// too loud quieter. This only affects how things sound for you.
-    #[serde(rename = "experimental.control_output_volume", default)]
-    pub control_output_volume: bool,
-}
-
-/// Configuration of audio in Zed.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[serde(default)]
-#[settings_key(key = "audio")]
-pub struct AudioSettingsContent {
-    /// Opt into the new audio system.
-    #[serde(rename = "experimental.rodio_audio", default)]
-    pub rodio_audio: bool, // default is false
-    /// Requires 'rodio_audio: true'
-    ///
-    /// Use the new audio systems automatic gain control for your microphone.
-    /// This affects how loud you sound to others.
-    #[serde(rename = "experimental.control_input_volume", default)]
-    pub control_input_volume: bool,
-    /// Requires 'rodio_audio: true'
-    ///
-    /// Use the new audio systems automatic gain control on everyone in the
-    /// call. This makes call members who are too quite louder and those who are
-    /// too loud quieter. This only affects how things sound for you.
-    #[serde(rename = "experimental.control_output_volume", default)]
     pub control_output_volume: bool,
 }
 
+/// Configuration of audio in Zed
 impl Settings for AudioSettings {
-    type FileContent = AudioSettingsContent;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let audio = &content.audio.as_ref().unwrap();
+        AudioSettings {
+            control_input_volume: audio.control_input_volume.unwrap(),
+            control_output_volume: audio.control_output_volume.unwrap(),
+            rodio_audio: audio.rodio_audio.unwrap(),
+        }
+    }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(audio) = content.audio.as_ref() else {
+            return;
+        };
+        self.control_input_volume
+            .merge_from(&audio.control_input_volume);
+        self.control_output_volume
+            .merge_from(&audio.control_output_volume);
+        self.rodio_audio.merge_from(&audio.rodio_audio);
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn import_from_vscode(
+        _vscode: &settings::VsCodeSettings,
+        _current: &mut settings::SettingsContent,
+    ) {
+    }
 }
 
 /// See docs on [LIVE_SETTINGS]

crates/auto_update/Cargo.toml πŸ”—

@@ -21,7 +21,6 @@ http_client.workspace = true
 log.workspace = true
 paths.workspace = true
 release_channel.workspace = true
-schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/auto_update/src/auto_update.rs πŸ”—

@@ -8,9 +8,8 @@ use gpui::{
 use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
 use paths::remote_servers_dir;
 use release_channel::{AppCommitSha, ReleaseChannel};
-use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
+use settings::{Settings, SettingsContent, SettingsStore};
 use smol::{fs, io::AsyncReadExt};
 use smol::{fs::File, process::Command};
 use std::{
@@ -113,47 +112,27 @@ impl Drop for MacOsUnmounter {
     }
 }
 
+#[derive(Clone, Copy, Debug)]
 struct AutoUpdateSetting(bool);
 
 /// Whether or not to automatically check for updates.
 ///
 /// Default: true
-#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)]
-#[settings_key(None)]
-#[settings_ui(group = "Auto Update")]
-struct AutoUpdateSettingContent {
-    pub auto_update: Option<bool>,
-}
-
 impl Settings for AutoUpdateSetting {
-    type FileContent = AutoUpdateSettingContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        let auto_update = [
-            sources.server,
-            sources.release_channel,
-            sources.operating_system,
-            sources.user,
-        ]
-        .into_iter()
-        .find_map(|value| value.and_then(|val| val.auto_update))
-        .or(sources.default.auto_update)
-        .ok_or_else(Self::missing_default)?;
-
-        Ok(Self(auto_update))
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        debug_assert_eq!(content.auto_update.unwrap(), true);
+        Self(content.auto_update.unwrap())
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
-        let mut cur = &mut Some(*current);
-        vscode.enum_setting("update.mode", &mut cur, |s| match s {
-            "none" | "manual" => Some(AutoUpdateSettingContent {
-                auto_update: Some(false),
-            }),
-            _ => Some(AutoUpdateSettingContent {
-                auto_update: Some(true),
-            }),
-        });
-        *current = cur.unwrap();
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        if let Some(auto_update) = content.auto_update {
+            self.0 = auto_update;
+        }
+    }
+
+    fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut SettingsContent) {
+        // We could match on vscode's update.mode here, but
+        // I think it's more important to have more people updating zed by default.
     }
 }
 
@@ -1002,7 +981,7 @@ mod tests {
     #[gpui::test]
     fn test_auto_update_defaults_to_true(cx: &mut TestAppContext) {
         cx.update(|cx| {
-            let mut store = SettingsStore::new(cx);
+            let mut store = SettingsStore::new(cx, &settings::default_settings());
             store
                 .set_default_settings(&default_settings(), cx)
                 .expect("Unable to set default settings");

crates/call/Cargo.toml πŸ”—

@@ -35,7 +35,6 @@ language.workspace = true
 log.workspace = true
 postage.workspace = true
 project.workspace = true
-schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
 telemetry.workspace = true

crates/call/src/call_settings.rs πŸ”—

@@ -1,36 +1,32 @@
-use anyhow::Result;
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::Settings;
+use util::MergeFrom;
 
-#[derive(Deserialize, Debug)]
+#[derive(Debug)]
 pub struct CallSettings {
     pub mute_on_join: bool,
     pub share_on_join: bool,
 }
 
-/// Configuration of voice calls in Zed.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "calls")]
-pub struct CallSettingsContent {
-    /// Whether the microphone should be muted when joining a channel or a call.
-    ///
-    /// Default: false
-    pub mute_on_join: Option<bool>,
-
-    /// Whether your current project should be shared when joining an empty channel.
-    ///
-    /// Default: false
-    pub share_on_join: Option<bool>,
-}
-
 impl Settings for CallSettings {
-    type FileContent = CallSettingsContent;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let call = content.calls.clone().unwrap();
+        CallSettings {
+            mute_on_join: call.mute_on_join.unwrap(),
+            share_on_join: call.share_on_join.unwrap(),
+        }
+    }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        if let Some(call) = content.calls.clone() {
+            self.mute_on_join.merge_from(&call.mute_on_join);
+            self.share_on_join.merge_from(&call.share_on_join);
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn import_from_vscode(
+        _vscode: &settings::VsCodeSettings,
+        _current: &mut settings::SettingsContent,
+    ) {
+    }
 }

crates/client/src/client.rs πŸ”—

@@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel};
 use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsContent, SettingsKey, SettingsUi};
 use std::{
     any::TypeId,
     convert::TryFrom,
@@ -50,7 +50,7 @@ use telemetry::Telemetry;
 use thiserror::Error;
 use tokio::net::TcpStream;
 use url::Url;
-use util::{ConnectionResult, ResultExt};
+use util::{ConnectionResult, MergeFrom, ResultExt};
 
 pub use rpc::*;
 pub use telemetry_events::Event;
@@ -96,29 +96,33 @@ actions!(
     ]
 );
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
-pub struct ClientSettingsContent {
-    server_url: Option<String>,
-}
-
 #[derive(Deserialize)]
 pub struct ClientSettings {
     pub server_url: String,
 }
 
 impl Settings for ClientSettings {
-    type FileContent = ClientSettingsContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        let mut result = sources.json_merge::<Self>()?;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
         if let Some(server_url) = &*ZED_SERVER_URL {
-            result.server_url.clone_from(server_url)
+            return Self {
+                server_url: server_url.clone(),
+            };
+        }
+        Self {
+            server_url: content.server_url.clone().unwrap(),
         }
-        Ok(result)
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
+        if ZED_SERVER_URL.is_some() {
+            return;
+        }
+        if let Some(server_url) = content.server_url.clone() {
+            self.server_url = server_url;
+        }
+    }
+
+    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
 }
 
 #[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
@@ -147,19 +151,19 @@ impl ProxySettings {
 }
 
 impl Settings for ProxySettings {
-    type FileContent = ProxySettingsContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        Ok(Self {
-            proxy: sources
-                .user
-                .or(sources.server)
-                .and_then(|value| value.proxy.clone())
-                .or(sources.default.proxy.clone()),
-        })
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        Self {
+            proxy: content.proxy.clone(),
+        }
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
+    fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
+        if let Some(proxy) = content.proxy.clone() {
+            self.proxy = Some(proxy)
+        }
+    }
+
+    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
         vscode.string_setting("http.proxy", &mut current.proxy);
     }
 }
@@ -538,37 +542,41 @@ pub struct TelemetrySettings {
     pub metrics: bool,
 }
 
-/// Control what info is collected by Zed.
-#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "telemetry")]
-pub struct TelemetrySettingsContent {
-    /// Send debug info like crash reports.
-    ///
-    /// Default: true
-    pub diagnostics: Option<bool>,
-    /// Send anonymized usage data like what languages you're using Zed with.
-    ///
-    /// Default: true
-    pub metrics: Option<bool>,
-}
-
 impl settings::Settings for TelemetrySettings {
-    type FileContent = TelemetrySettingsContent;
+    fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self {
+        Self {
+            diagnostics: content.telemetry.as_ref().unwrap().diagnostics.unwrap(),
+            metrics: content.telemetry.as_ref().unwrap().metrics.unwrap(),
+        }
+    }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
+        let Some(telemetry) = &content.telemetry else {
+            return;
+        };
+        self.diagnostics.merge_from(&telemetry.diagnostics);
+        self.metrics.merge_from(&telemetry.metrics);
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
-        vscode.enum_setting("telemetry.telemetryLevel", &mut current.metrics, |s| {
+    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
+        let mut telemetry = settings::TelemetrySettingsContent::default();
+        vscode.enum_setting("telemetry.telemetryLevel", &mut telemetry.metrics, |s| {
             Some(s == "all")
         });
-        vscode.enum_setting("telemetry.telemetryLevel", &mut current.diagnostics, |s| {
-            Some(matches!(s, "all" | "error" | "crash"))
-        });
+        vscode.enum_setting(
+            "telemetry.telemetryLevel",
+            &mut telemetry.diagnostics,
+            |s| Some(matches!(s, "all" | "error" | "crash")),
+        );
         // we could translate telemetry.telemetryLevel, but just because users didn't want
         // to send microsoft telemetry doesn't mean they don't want to send it to zed. their
         // all/error/crash/off correspond to combinations of our "diagnostics" and "metrics".
+        if let Some(diagnostics) = telemetry.diagnostics {
+            current.telemetry.get_or_insert_default().diagnostics = Some(diagnostics)
+        }
+        if let Some(metrics) = telemetry.metrics {
+            current.telemetry.get_or_insert_default().metrics = Some(metrics)
+        }
     }
 }
 

crates/collab/src/tests/editor_tests.rs πŸ”—

@@ -4,7 +4,7 @@ use crate::{
 };
 use call::ActiveCall;
 use editor::{
-    DocumentColorsRenderMode, Editor, EditorSettings, RowInfo, SelectionEffects,
+    DocumentColorsRenderMode, Editor, RowInfo, SelectionEffects,
     actions::{
         ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
         ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
@@ -18,20 +18,16 @@ use fs::Fs;
 use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
 use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
 use indoc::indoc;
-use language::{
-    FakeLspAdapter,
-    language_settings::{AllLanguageSettings, InlayHintSettings},
-};
+use language::FakeLspAdapter;
 use lsp::LSP_REQUEST_TIMEOUT;
 use project::{
     ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
     lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
-    project_settings::{InlineBlameSettings, ProjectSettings},
 };
 use recent_projects::disconnected_overlay::DisconnectedOverlay;
 use rpc::RECEIVE_TIMEOUT;
 use serde_json::json;
-use settings::SettingsStore;
+use settings::{InlayHintSettingsContent, InlineBlameSettings, SettingsStore};
 use std::{
     collections::BTreeSet,
     ops::{Deref as _, Range},
@@ -1789,35 +1785,37 @@ async fn test_mutual_editor_inlay_hint_cache_update(
 
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.defaults.inlay_hints = Some(InlayHintSettings {
-                    enabled: true,
-                    show_value_hints: true,
-                    edit_debounce_ms: 0,
-                    scroll_debounce_ms: 0,
-                    show_type_hints: true,
-                    show_parameter_hints: false,
-                    show_other_hints: true,
-                    show_background: false,
-                    toggle_on_modifiers_press: None,
-                })
+            store.update_user_settings(cx, |settings| {
+                settings.project.all_languages.defaults.inlay_hints =
+                    Some(InlayHintSettingsContent {
+                        enabled: Some(true),
+                        show_value_hints: Some(true),
+                        edit_debounce_ms: Some(0),
+                        scroll_debounce_ms: Some(0),
+                        show_type_hints: Some(true),
+                        show_parameter_hints: Some(false),
+                        show_other_hints: Some(true),
+                        show_background: Some(false),
+                        toggle_on_modifiers_press: None,
+                    })
             });
         });
     });
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.defaults.inlay_hints = Some(InlayHintSettings {
-                    show_value_hints: true,
-                    enabled: true,
-                    edit_debounce_ms: 0,
-                    scroll_debounce_ms: 0,
-                    show_type_hints: true,
-                    show_parameter_hints: false,
-                    show_other_hints: true,
-                    show_background: false,
-                    toggle_on_modifiers_press: None,
-                })
+            store.update_user_settings(cx, |settings| {
+                settings.project.all_languages.defaults.inlay_hints =
+                    Some(InlayHintSettingsContent {
+                        show_value_hints: Some(true),
+                        enabled: Some(true),
+                        edit_debounce_ms: Some(0),
+                        scroll_debounce_ms: Some(0),
+                        show_type_hints: Some(true),
+                        show_parameter_hints: Some(false),
+                        show_other_hints: Some(true),
+                        show_background: Some(false),
+                        toggle_on_modifiers_press: None,
+                    })
             });
         });
     });
@@ -2039,35 +2037,37 @@ async fn test_inlay_hint_refresh_is_forwarded(
 
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.defaults.inlay_hints = Some(InlayHintSettings {
-                    show_value_hints: true,
-                    enabled: false,
-                    edit_debounce_ms: 0,
-                    scroll_debounce_ms: 0,
-                    show_type_hints: false,
-                    show_parameter_hints: false,
-                    show_other_hints: false,
-                    show_background: false,
-                    toggle_on_modifiers_press: None,
-                })
+            store.update_user_settings(cx, |settings| {
+                settings.project.all_languages.defaults.inlay_hints =
+                    Some(InlayHintSettingsContent {
+                        show_value_hints: Some(true),
+                        enabled: Some(false),
+                        edit_debounce_ms: Some(0),
+                        scroll_debounce_ms: Some(0),
+                        show_type_hints: Some(false),
+                        show_parameter_hints: Some(false),
+                        show_other_hints: Some(false),
+                        show_background: Some(false),
+                        toggle_on_modifiers_press: None,
+                    })
             });
         });
     });
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.defaults.inlay_hints = Some(InlayHintSettings {
-                    show_value_hints: true,
-                    enabled: true,
-                    edit_debounce_ms: 0,
-                    scroll_debounce_ms: 0,
-                    show_type_hints: true,
-                    show_parameter_hints: true,
-                    show_other_hints: true,
-                    show_background: false,
-                    toggle_on_modifiers_press: None,
-                })
+            store.update_user_settings(cx, |settings| {
+                settings.project.all_languages.defaults.inlay_hints =
+                    Some(InlayHintSettingsContent {
+                        show_value_hints: Some(true),
+                        enabled: Some(true),
+                        edit_debounce_ms: Some(0),
+                        scroll_debounce_ms: Some(0),
+                        show_type_hints: Some(true),
+                        show_parameter_hints: Some(true),
+                        show_other_hints: Some(true),
+                        show_background: Some(false),
+                        toggle_on_modifiers_press: None,
+                    })
             });
         });
     });
@@ -2241,15 +2241,15 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
 
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.lsp_document_colors = Some(DocumentColorsRenderMode::None);
+            store.update_user_settings(cx, |settings| {
+                settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::None);
             });
         });
     });
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
+            store.update_user_settings(cx, |settings| {
+                settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
             });
         });
     });
@@ -2422,8 +2422,8 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
 
     cx_b.update(|_, cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
+            store.update_user_settings(cx, |settings| {
+                settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
             });
         });
     });
@@ -2450,8 +2450,8 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
 
     cx_b.update(|_, cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
+            store.update_user_settings(cx, |settings| {
+                settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
             });
         });
     });
@@ -2478,8 +2478,8 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
 
     cx_a.update(|_, cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
+            store.update_user_settings(cx, |settings| {
+                settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
             });
         });
     });
@@ -3306,20 +3306,20 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
     cx_b.update(editor::init);
     // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
     let inline_blame_off_settings = Some(InlineBlameSettings {
-        enabled: false,
+        enabled: Some(false),
         ..Default::default()
     });
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<ProjectSettings>(cx, |settings| {
-                settings.git.inline_blame = inline_blame_off_settings;
+            store.update_user_settings(cx, |settings| {
+                settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
             });
         });
     });
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<ProjectSettings>(cx, |settings| {
-                settings.git.inline_blame = inline_blame_off_settings;
+            store.update_user_settings(cx, |settings| {
+                settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
             });
         });
     });

crates/collab/src/tests/following_tests.rs πŸ”—

@@ -12,7 +12,6 @@ use gpui::{
     VisualContext, VisualTestContext, point,
 };
 use language::Capability;
-use project::WorktreeSettings;
 use rpc::proto::PeerId;
 use serde_json::json;
 use settings::SettingsStore;
@@ -1710,8 +1709,9 @@ async fn test_following_into_excluded_file(
     for cx in [&mut cx_a, &mut cx_b] {
         cx.update(|cx| {
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |settings| {
-                    settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
+                store.update_user_settings(cx, |settings| {
+                    settings.project.worktree.file_scan_exclusions =
+                        Some(vec!["**/.git".to_string()]);
                 });
             });
         });

crates/collab/src/tests/integration_tests.rs πŸ”—

@@ -22,9 +22,7 @@ use gpui::{
 use language::{
     Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
     LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
-    language_settings::{
-        AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
-    },
+    language_settings::{Formatter, FormatterList, SelectedFormatter},
     tree_sitter_rust, tree_sitter_typescript,
 };
 use lsp::{LanguageServerId, OneOf};
@@ -38,7 +36,7 @@ use project::{
 use prompt_store::PromptBuilder;
 use rand::prelude::*;
 use serde_json::json;
-use settings::SettingsStore;
+use settings::{PrettierSettingsContent, SettingsStore};
 use std::{
     cell::{Cell, RefCell},
     env, future, mem,
@@ -3507,10 +3505,14 @@ async fn test_local_settings(
         assert_eq!(
             store
                 .local_settings(worktree_b.read(cx).id())
+                .map(|(path, content)| (
+                    path,
+                    content.all_languages.defaults.tab_size.map(Into::into)
+                ))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
-                (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
+                (Path::new("").into(), Some(2)),
+                (Path::new("a").into(), Some(8)),
             ]
         )
     });
@@ -3526,10 +3528,14 @@ async fn test_local_settings(
         assert_eq!(
             store
                 .local_settings(worktree_b.read(cx).id())
+                .map(|(path, content)| (
+                    path,
+                    content.all_languages.defaults.tab_size.map(Into::into)
+                ))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new("").into(), r#"{}"#.to_string()),
-                (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
+                (Path::new("").into(), None),
+                (Path::new("a").into(), Some(8)),
             ]
         )
     });
@@ -3555,10 +3561,14 @@ async fn test_local_settings(
         assert_eq!(
             store
                 .local_settings(worktree_b.read(cx).id())
+                .map(|(path, content)| (
+                    path,
+                    content.all_languages.defaults.tab_size.map(Into::into)
+                ))
                 .collect::<Vec<_>>(),
             &[
-                (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
-                (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
+                (Path::new("a").into(), Some(8)),
+                (Path::new("b").into(), Some(4)),
             ]
         )
     });
@@ -3587,8 +3597,9 @@ async fn test_local_settings(
         assert_eq!(
             store
                 .local_settings(worktree_b.read(cx).id())
+                .map(|(path, content)| (path, content.all_languages.defaults.hard_tabs))
                 .collect::<Vec<_>>(),
-            &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
+            &[(Path::new("a").into(), Some(true))],
         )
     });
 }
@@ -4598,15 +4609,15 @@ async fn test_formatting_buffer(
         // host's configuration is honored as opposed to using the guest's settings.
         cx_a.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<AllLanguageSettings>(cx, |file| {
-                    file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
-                        Formatter::External {
+                store.update_user_settings(cx, |file| {
+                    file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
+                        FormatterList::Single(Formatter::External {
                             command: "awk".into(),
                             arguments: Some(
                                 vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
                             ),
-                        },
-                    )));
+                        }),
+                    ));
                 });
             });
         });
@@ -4694,24 +4705,24 @@ async fn test_prettier_formatting_buffer(
 
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
-                file.defaults.formatter = Some(SelectedFormatter::Auto);
-                file.defaults.prettier = Some(PrettierSettings {
-                    allowed: true,
-                    ..PrettierSettings::default()
+            store.update_user_settings(cx, |file| {
+                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
+                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
+                    allowed: Some(true),
+                    ..Default::default()
                 });
             });
         });
     });
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
-                file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
-                    Formatter::LanguageServer { name: None },
-                )));
-                file.defaults.prettier = Some(PrettierSettings {
-                    allowed: true,
-                    ..PrettierSettings::default()
+            store.update_user_settings(cx, |file| {
+                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
+                    FormatterList::Single(Formatter::LanguageServer { name: None }),
+                ));
+                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
+                    allowed: Some(true),
+                    ..Default::default()
                 });
             });
         });

crates/collab/src/tests/remote_editing_collaboration_tests.rs πŸ”—

@@ -14,10 +14,7 @@ use gpui::{
 use http_client::BlockedHttpClient;
 use language::{
     FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
-    language_settings::{
-        AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
-        language_settings,
-    },
+    language_settings::{Formatter, FormatterList, SelectedFormatter, language_settings},
     tree_sitter_typescript,
 };
 use node_runtime::NodeRuntime;
@@ -30,7 +27,7 @@ use remote::RemoteClient;
 use remote_server::{HeadlessAppState, HeadlessProject};
 use rpc::proto;
 use serde_json::json;
-use settings::SettingsStore;
+use settings::{PrettierSettingsContent, SettingsStore};
 use std::{
     path::Path,
     sync::{Arc, atomic::AtomicUsize},
@@ -499,24 +496,24 @@ async fn test_ssh_collaboration_formatting_with_prettier(
 
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
-                file.defaults.formatter = Some(SelectedFormatter::Auto);
-                file.defaults.prettier = Some(PrettierSettings {
-                    allowed: true,
-                    ..PrettierSettings::default()
+            store.update_user_settings(cx, |file| {
+                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
+                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
+                    allowed: Some(true),
+                    ..Default::default()
                 });
             });
         });
     });
     cx_b.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
-                file.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
-                    Formatter::LanguageServer { name: None },
-                )));
-                file.defaults.prettier = Some(PrettierSettings {
-                    allowed: true,
-                    ..PrettierSettings::default()
+            store.update_user_settings(cx, |file| {
+                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
+                    FormatterList::Single(Formatter::LanguageServer { name: None }),
+                ));
+                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
+                    allowed: Some(true),
+                    ..Default::default()
                 });
             });
         });
@@ -556,11 +553,11 @@ async fn test_ssh_collaboration_formatting_with_prettier(
 
     cx_a.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
-                file.defaults.formatter = Some(SelectedFormatter::Auto);
-                file.defaults.prettier = Some(PrettierSettings {
-                    allowed: true,
-                    ..PrettierSettings::default()
+            store.update_user_settings(cx, |file| {
+                file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
+                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
+                    allowed: Some(true),
+                    ..Default::default()
                 });
             });
         });

crates/collab_ui/Cargo.toml πŸ”—

@@ -47,7 +47,6 @@ picker.workspace = true
 project.workspace = true
 release_channel.workspace = true
 rpc.workspace = true
-schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/collab_ui/src/collab_panel.rs πŸ”—

@@ -2993,11 +2993,9 @@ impl Panel for CollabPanel {
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        settings::update_settings_file::<CollaborationPanelSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings, _| settings.dock = Some(position),
-        );
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            settings.collaboration_panel.get_or_insert_default().dock = Some(position.into())
+        });
     }
 
     fn size(&self, _window: &Window, cx: &App) -> Pixels {

crates/collab_ui/src/collab_ui.rs πŸ”—

@@ -11,7 +11,6 @@ use gpui::{
     App, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds,
     WindowDecorations, WindowKind, WindowOptions, point,
 };
-use panel_settings::MessageEditorSettings;
 pub use panel_settings::{CollaborationPanelSettings, NotificationPanelSettings};
 use release_channel::ReleaseChannel;
 use settings::Settings;
@@ -21,7 +20,6 @@ use workspace::AppState;
 pub fn init(app_state: &Arc<AppState>, cx: &mut App) {
     CollaborationPanelSettings::register(cx);
     NotificationPanelSettings::register(cx);
-    MessageEditorSettings::register(cx);
 
     channel_view::init(cx);
     collab_panel::init(cx);

crates/collab_ui/src/notification_panel.rs πŸ”—

@@ -621,11 +621,9 @@ impl Panel for NotificationPanel {
     }
 
     fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
-        settings::update_settings_file::<NotificationPanelSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings, _| settings.dock = Some(position),
-        );
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            settings.notification_panel.get_or_insert_default().dock = Some(position.into())
+        });
     }
 
     fn size(&self, _: &Window, cx: &App) -> Pixels {

crates/collab_ui/src/panel_settings.rs πŸ”—

@@ -1,102 +1,72 @@
 use gpui::Pixels;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::Settings;
+use ui::px;
+use util::MergeFrom as _;
 use workspace::dock::DockPosition;
 
-#[derive(Deserialize, Debug)]
+#[derive(Debug)]
 pub struct CollaborationPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
     pub default_width: Pixels,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "collaboration_panel")]
-pub struct PanelSettingsContent {
-    /// Whether to show the panel button in the status bar.
-    ///
-    /// Default: true
-    pub button: Option<bool>,
-    /// Where to dock the panel.
-    ///
-    /// Default: left
-    pub dock: Option<DockPosition>,
-    /// Default width of the panel in pixels.
-    ///
-    /// Default: 240
-    pub default_width: Option<f32>,
-}
-
-#[derive(Deserialize, Debug)]
+#[derive(Debug)]
 pub struct NotificationPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
     pub default_width: Pixels,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "notification_panel")]
-pub struct NotificationPanelSettingsContent {
-    /// Whether to show the panel button in the status bar.
-    ///
-    /// Default: true
-    pub button: Option<bool>,
-    /// Where to dock the panel.
-    ///
-    /// Default: right
-    pub dock: Option<DockPosition>,
-    /// Default width of the panel in pixels.
-    ///
-    /// Default: 300
-    pub default_width: Option<f32>,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "message_editor")]
-pub struct MessageEditorSettings {
-    /// Whether to automatically replace emoji shortcodes with emoji characters.
-    /// For example: typing `:wave:` gets replaced with `πŸ‘‹`.
-    ///
-    /// Default: false
-    pub auto_replace_emoji_shortcode: Option<bool>,
-}
-
 impl Settings for CollaborationPanelSettings {
-    type FileContent = PanelSettingsContent;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+        let panel = content.collaboration_panel.as_ref().unwrap();
 
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _: &mut gpui::App,
-    ) -> anyhow::Result<Self> {
-        sources.json_merge()
+        Self {
+            button: panel.button.unwrap(),
+            dock: panel.dock.unwrap().into(),
+            default_width: panel.default_width.map(px).unwrap(),
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
-}
-
-impl Settings for NotificationPanelSettings {
-    type FileContent = NotificationPanelSettingsContent;
-
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _: &mut gpui::App,
-    ) -> anyhow::Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) {
+        if let Some(panel) = content.collaboration_panel.as_ref() {
+            self.button.merge_from(&panel.button);
+            self.default_width
+                .merge_from(&panel.default_width.map(Pixels::from));
+            self.dock.merge_from(&panel.dock.map(Into::into));
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn import_from_vscode(
+        _vscode: &settings::VsCodeSettings,
+        _content: &mut settings::SettingsContent,
+    ) {
+    }
 }
 
-impl Settings for MessageEditorSettings {
-    type FileContent = MessageEditorSettings;
+impl Settings for NotificationPanelSettings {
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+        let panel = content.notification_panel.as_ref().unwrap();
+        return Self {
+            button: panel.button.unwrap(),
+            dock: panel.dock.unwrap().into(),
+            default_width: panel.default_width.map(px).unwrap(),
+        };
+    }
 
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _: &mut gpui::App,
-    ) -> anyhow::Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) {
+        let Some(panel) = content.notification_panel.as_ref() else {
+            return;
+        };
+        self.button.merge_from(&panel.button);
+        self.dock.merge_from(&panel.dock.map(Into::into));
+        self.default_width.merge_from(&panel.default_width.map(px));
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn import_from_vscode(
+        _vscode: &settings::VsCodeSettings,
+        _current: &mut settings::SettingsContent,
+    ) {
+    }
 }

crates/context_server/Cargo.toml πŸ”—

@@ -25,8 +25,9 @@ net.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 schemars.workspace = true
-serde.workspace = true
 serde_json.workspace = true
+serde.workspace = true
+settings.workspace = true
 smol.workspace = true
 tempfile.workspace = true
 url = { workspace = true, features = ["serde"] }

crates/context_server/src/context_server.rs πŸ”—

@@ -12,12 +12,9 @@ use std::{fmt::Display, path::PathBuf};
 
 use anyhow::Result;
 use client::Client;
-use collections::HashMap;
 use gpui::AsyncApp;
 use parking_lot::RwLock;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use util::redact::should_redact;
+pub use settings::ContextServerCommand;
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct ContextServerId(pub Arc<str>);
@@ -28,32 +25,6 @@ impl Display for ContextServerId {
     }
 }
 
-#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
-pub struct ContextServerCommand {
-    #[serde(rename = "command")]
-    pub path: PathBuf,
-    pub args: Vec<String>,
-    pub env: Option<HashMap<String, String>>,
-    /// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified.
-    pub timeout: Option<u64>,
-}
-
-impl std::fmt::Debug for ContextServerCommand {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let filtered_env = self.env.as_ref().map(|env| {
-            env.iter()
-                .map(|(k, v)| (k, if should_redact(k) { "[REDACTED]" } else { v }))
-                .collect::<Vec<_>>()
-        });
-
-        f.debug_struct("ContextServerCommand")
-            .field("path", &self.path)
-            .field("args", &self.args)
-            .field("env", &filtered_env)
-            .finish()
-    }
-}
-
 enum ContextServerTransport {
     Stdio(ContextServerCommand, Option<PathBuf>),
     Custom(Arc<dyn crate::transport::Transport>),

crates/copilot/src/copilot_completion_provider.rs πŸ”—

@@ -281,14 +281,11 @@ mod tests {
     use indoc::indoc;
     use language::{
         Point,
-        language_settings::{
-            AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, LspInsertMode,
-            WordsCompletionMode,
-        },
+        language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},
     };
     use project::Project;
     use serde_json::json;
-    use settings::SettingsStore;
+    use settings::{AllLanguageSettingsContent, SettingsStore};
     use std::future::Future;
     use util::{
         path,
@@ -299,12 +296,11 @@ mod tests {
     async fn test_copilot(executor: BackgroundExecutor, cx: &mut TestAppContext) {
         // flaky
         init_test(cx, |settings| {
-            settings.defaults.completions = Some(CompletionSettings {
-                words: WordsCompletionMode::Disabled,
-                words_min_length: 0,
-                lsp: true,
-                lsp_fetch_timeout_ms: 0,
-                lsp_insert_mode: LspInsertMode::Insert,
+            settings.defaults.completions = Some(CompletionSettingsContent {
+                words: Some(WordsCompletionMode::Disabled),
+                words_min_length: Some(0),
+                lsp_insert_mode: Some(LspInsertMode::Insert),
+                ..Default::default()
             });
         });
 
@@ -532,12 +528,11 @@ mod tests {
     ) {
         // flaky
         init_test(cx, |settings| {
-            settings.defaults.completions = Some(CompletionSettings {
-                words: WordsCompletionMode::Disabled,
-                words_min_length: 0,
-                lsp: true,
-                lsp_fetch_timeout_ms: 0,
-                lsp_insert_mode: LspInsertMode::Insert,
+            settings.defaults.completions = Some(CompletionSettingsContent {
+                words: Some(WordsCompletionMode::Disabled),
+                words_min_length: Some(0),
+                lsp_insert_mode: Some(LspInsertMode::Insert),
+                ..Default::default()
             });
         });
 
@@ -1128,7 +1123,7 @@ mod tests {
             Project::init_settings(cx);
             workspace::init_settings(cx);
             SettingsStore::update_global(cx, |store: &mut SettingsStore, cx| {
-                store.update_user_settings::<AllLanguageSettings>(cx, f);
+                store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
             });
         });
     }

crates/dap/src/debugger_settings.rs πŸ”—

@@ -1,28 +1,12 @@
 use dap_types::SteppingGranularity;
-use gpui::{App, Global};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use gpui::App;
+use settings::{Settings, SettingsContent};
+use util::MergeFrom;
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-pub enum DebugPanelDockPosition {
-    Left,
-    Bottom,
-    Right,
-}
-
-#[derive(Serialize, Deserialize, JsonSchema, Clone, SettingsUi, SettingsKey)]
-#[serde(default)]
-// todo(settings_ui) @ben: I'm pretty sure not having the fields be optional here is a bug,
-// it means the defaults will override previously set values if a single key is missing
-#[settings_ui(group = "Debugger")]
-#[settings_key(key = "debugger")]
 pub struct DebuggerSettings {
     /// Determines the stepping granularity.
     ///
     /// Default: line
-    #[settings_ui(skip)]
     pub stepping_granularity: SteppingGranularity,
     /// Whether the breakpoints should be reused across Zed sessions.
     ///
@@ -47,31 +31,53 @@ pub struct DebuggerSettings {
     /// The dock position of the debug panel
     ///
     /// Default: Bottom
-    pub dock: DebugPanelDockPosition,
+    pub dock: settings::DockPosition,
 }
 
-impl Default for DebuggerSettings {
-    fn default() -> Self {
+impl Settings for DebuggerSettings {
+    fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self {
+        let content = content.debugger.clone().unwrap();
         Self {
-            button: true,
-            save_breakpoints: true,
-            stepping_granularity: SteppingGranularity::Line,
-            timeout: 2000,
-            log_dap_communications: true,
-            format_dap_log_messages: true,
-            dock: DebugPanelDockPosition::Bottom,
+            stepping_granularity: dap_granularity_from_settings(
+                content.stepping_granularity.unwrap(),
+            ),
+            save_breakpoints: content.save_breakpoints.unwrap(),
+            button: content.button.unwrap(),
+            timeout: content.timeout.unwrap(),
+            log_dap_communications: content.log_dap_communications.unwrap(),
+            format_dap_log_messages: content.format_dap_log_messages.unwrap(),
+            dock: content.dock.unwrap(),
         }
     }
-}
 
-impl Settings for DebuggerSettings {
-    type FileContent = Self;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
+        let Some(content) = &content.debugger else {
+            return;
+        };
+        self.stepping_granularity.merge_from(
+            &content
+                .stepping_granularity
+                .map(dap_granularity_from_settings),
+        );
+        self.save_breakpoints.merge_from(&content.save_breakpoints);
+        self.button.merge_from(&content.button);
+        self.timeout.merge_from(&content.timeout);
+        self.log_dap_communications
+            .merge_from(&content.log_dap_communications);
+        self.format_dap_log_messages
+            .merge_from(&content.format_dap_log_messages);
+        self.dock.merge_from(&content.dock);
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
 }
 
-impl Global for DebuggerSettings {}
+fn dap_granularity_from_settings(
+    granularity: settings::SteppingGranularity,
+) -> dap_types::SteppingGranularity {
+    match granularity {
+        settings::SteppingGranularity::Instruction => dap_types::SteppingGranularity::Instruction,
+        settings::SteppingGranularity::Line => dap_types::SteppingGranularity::Line,
+        settings::SteppingGranularity::Statement => dap_types::SteppingGranularity::Statement,
+    }
+}

crates/debugger_ui/src/debugger_panel.rs πŸ”—

@@ -12,7 +12,6 @@ use crate::{
 use anyhow::{Context as _, Result, anyhow};
 use collections::IndexMap;
 use dap::adapters::DebugAdapterName;
-use dap::debugger_settings::DebugPanelDockPosition;
 use dap::{DapRegistry, StartDebuggingRequestArguments};
 use dap::{client::SessionId, debugger_settings::DebuggerSettings};
 use editor::Editor;
@@ -1400,11 +1399,7 @@ impl Panel for DebugPanel {
     }
 
     fn position(&self, _window: &Window, cx: &App) -> DockPosition {
-        match DebuggerSettings::get_global(cx).dock {
-            DebugPanelDockPosition::Left => DockPosition::Left,
-            DebugPanelDockPosition::Bottom => DockPosition::Bottom,
-            DebugPanelDockPosition::Right => DockPosition::Right,
-        }
+        DebuggerSettings::get_global(cx).dock.into()
     }
 
     fn position_is_valid(&self, _: DockPosition) -> bool {
@@ -1426,18 +1421,9 @@ impl Panel for DebugPanel {
             });
         }
 
-        settings::update_settings_file::<DebuggerSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings, _| {
-                let dock = match position {
-                    DockPosition::Left => DebugPanelDockPosition::Left,
-                    DockPosition::Bottom => DebugPanelDockPosition::Bottom,
-                    DockPosition::Right => DebugPanelDockPosition::Right,
-                };
-                settings.dock = dock;
-            },
-        );
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            settings.debugger.get_or_insert_default().dock = Some(position.into());
+        });
     }
 
     fn size(&self, _window: &Window, _: &App) -> Pixels {

crates/edit_prediction_button/src/edit_prediction_button.rs πŸ”—

@@ -910,8 +910,10 @@ async fn open_disabled_globs_setting_in_editor(
             let settings = cx.global::<SettingsStore>();
 
             // Ensure that we always have "edit_predictions { "disabled_globs": [] }"
-            let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
-                file.edit_predictions
+            let edits = settings.edits_for_update(&text, |file| {
+                file.project
+                    .all_languages
+                    .edit_predictions
                     .get_or_insert_with(Default::default)
                     .disabled_globs
                     .get_or_insert_with(Vec::new);
@@ -948,9 +950,12 @@ async fn open_disabled_globs_setting_in_editor(
 }
 
 fn set_completion_provider(fs: Arc<dyn Fs>, cx: &mut App, provider: EditPredictionProvider) {
-    update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
-        file.features
-            .get_or_insert(Default::default())
+    update_settings_file(fs, cx, move |settings, _| {
+        settings
+            .project
+            .all_languages
+            .features
+            .get_or_insert_default()
             .edit_prediction_provider = Some(provider);
     });
 }
@@ -962,18 +967,24 @@ fn toggle_show_edit_predictions_for_language(
 ) {
     let show_edit_predictions =
         all_language_settings(None, cx).show_edit_predictions(Some(&language), cx);
-    update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
-        file.languages
+    update_settings_file(fs, cx, move |settings, _| {
+        settings
+            .project
+            .all_languages
+            .languages
             .0
-            .entry(language.name())
+            .entry(language.name().0)
             .or_default()
             .show_edit_predictions = Some(!show_edit_predictions);
     });
 }
 
 fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut App) {
-    update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
-        file.features
+    update_settings_file(fs, cx, move |settings, _| {
+        settings
+            .project
+            .all_languages
+            .features
             .get_or_insert(Default::default())
             .edit_prediction_provider = Some(EditPredictionProvider::None);
     });
@@ -984,13 +995,14 @@ fn toggle_edit_prediction_mode(fs: Arc<dyn Fs>, mode: EditPredictionsMode, cx: &
     let current_mode = settings.edit_predictions_mode();
 
     if current_mode != mode {
-        update_settings_file::<AllLanguageSettings>(fs, cx, move |settings, _cx| {
-            if let Some(edit_predictions) = settings.edit_predictions.as_mut() {
-                edit_predictions.mode = mode;
+        update_settings_file(fs, cx, move |settings, _cx| {
+            if let Some(edit_predictions) = settings.project.all_languages.edit_predictions.as_mut()
+            {
+                edit_predictions.mode = Some(mode);
             } else {
-                settings.edit_predictions =
-                    Some(language_settings::EditPredictionSettingsContent {
-                        mode,
+                settings.project.all_languages.edit_predictions =
+                    Some(settings::EditPredictionSettingsContent {
+                        mode: Some(mode),
                         ..Default::default()
                     });
             }

crates/editor/src/code_completion_tests.rs πŸ”—

@@ -1,9 +1,10 @@
-use crate::{code_context_menus::CompletionsMenu, editor_settings::SnippetSortOrder};
+use crate::code_context_menus::CompletionsMenu;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::TestAppContext;
 use language::CodeLabel;
 use lsp::{CompletionItem, CompletionItemKind, LanguageServerId};
 use project::{Completion, CompletionSource};
+use settings::SnippetSortOrder;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 use text::Anchor;

crates/editor/src/code_context_menus.rs πŸ”—

@@ -32,7 +32,6 @@ use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*};
 use util::ResultExt;
 
 use crate::CodeActionSource;
-use crate::editor_settings::SnippetSortOrder;
 use crate::hover_popover::{hover_markdown_style, open_markdown_url};
 use crate::{
     CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
@@ -40,6 +39,7 @@ use crate::{
     actions::{ConfirmCodeAction, ConfirmCompletion},
     split_words, styled_runs_for_code_label,
 };
+use settings::SnippetSortOrder;
 
 pub const MENU_GAP: Pixels = px(4.);
 pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);

crates/editor/src/display_map.rs πŸ”—

@@ -1529,12 +1529,11 @@ pub mod tests {
     use language::{
         Buffer, Diagnostic, DiagnosticEntry, DiagnosticSet, Language, LanguageConfig,
         LanguageMatcher,
-        language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
     };
     use lsp::LanguageServerId;
     use project::Project;
     use rand::{Rng, prelude::*};
-    use settings::SettingsStore;
+    use settings::{SettingsContent, SettingsStore};
     use smol::stream::StreamExt;
     use std::{env, sync::Arc};
     use text::PointUtf16;
@@ -1564,7 +1563,9 @@ pub mod tests {
         log::info!("wrap width: {:?}", wrap_width);
 
         cx.update(|cx| {
-            init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
+            init_test(cx, |s| {
+                s.project.all_languages.defaults.tab_size = NonZeroU32::new(tab_size)
+            });
         });
 
         let buffer = cx.update(|cx| {
@@ -1623,8 +1624,9 @@ pub mod tests {
                     log::info!("setting tab size to {:?}", tab_size);
                     cx.update(|cx| {
                         cx.update_global::<SettingsStore, _>(|store, cx| {
-                            store.update_user_settings::<AllLanguageSettings>(cx, |s| {
-                                s.defaults.tab_size = NonZeroU32::new(tab_size);
+                            store.update_user_settings(cx, |s| {
+                                s.project.all_languages.defaults.tab_size =
+                                    NonZeroU32::new(tab_size);
                             });
                         });
                     });
@@ -2084,7 +2086,11 @@ pub mod tests {
         );
         language.set_theme(&theme);
 
-        cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
+        cx.update(|cx| {
+            init_test(cx, |s| {
+                s.project.all_languages.defaults.tab_size = Some(2.try_into().unwrap())
+            })
+        });
 
         let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
         cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
@@ -2910,7 +2916,7 @@ pub mod tests {
         chunks
     }
 
-    fn init_test(cx: &mut App, f: impl Fn(&mut AllLanguageSettingsContent)) {
+    fn init_test(cx: &mut App, f: impl Fn(&mut SettingsContent)) {
         let settings = SettingsStore::test(cx);
         cx.set_global(settings);
         workspace::init_settings(cx);
@@ -2919,7 +2925,7 @@ pub mod tests {
         Project::init_settings(cx);
         theme::init(LoadThemes::JustBase, cx);
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, f);
+            store.update_user_settings(cx, f);
         });
     }
 }

crates/editor/src/editor.rs πŸ”—

@@ -160,9 +160,7 @@ use project::{
     },
     git_store::{GitStoreEvent, RepositoryEvent},
     lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
-    project_settings::{
-        DiagnosticSeverity, GitGutterSetting, GoToDiagnosticSeverityFilter, ProjectSettings,
-    },
+    project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter, ProjectSettings},
 };
 use rand::seq::SliceRandom;
 use rpc::{ErrorCode, ErrorExt, proto::PeerId};
@@ -171,7 +169,7 @@ use selections_collection::{
     MutableSelectionsCollection, SelectionsCollection, resolve_selections,
 };
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file};
+use settings::{GitGutterSetting, Settings, SettingsLocation, SettingsStore, update_settings_file};
 use smallvec::{SmallVec, smallvec};
 use snippet::Snippet;
 use std::{
@@ -2189,7 +2187,7 @@ impl Editor {
             show_selection_menu: None,
             show_git_blame_inline_delay_task: None,
             git_blame_inline_enabled: full_mode
-                && ProjectSettings::get_global(cx).git.inline_blame_enabled(),
+                && ProjectSettings::get_global(cx).git.inline_blame.enabled,
             render_diff_hunk_controls: Arc::new(render_diff_hunk_controls),
             serialize_dirty_buffers: !is_minimap
                 && ProjectSettings::get_global(cx)
@@ -5591,8 +5589,9 @@ impl Editor {
             .language_at(buffer_position)
             .map(|language| language.name());
 
-        let completion_settings =
-            language_settings(language.clone(), buffer_snapshot.file(), cx).completions;
+        let completion_settings = language_settings(language.clone(), buffer_snapshot.file(), cx)
+            .completions
+            .clone();
 
         let show_completion_documentation = buffer_snapshot
             .settings_at(buffer_position, cx)
@@ -19008,8 +19007,8 @@ impl Editor {
         };
         let fs = workspace.read(cx).app_state().fs.clone();
         let current_show = TabBarSettings::get_global(cx).show;
-        update_settings_file::<TabBarSettings>(fs, cx, move |setting, _| {
-            setting.show = Some(!current_show);
+        update_settings_file(fs, cx, move |setting, _| {
+            setting.tab_bar.get_or_insert_default().show = Some(!current_show);
         });
     }
 
@@ -20723,7 +20722,7 @@ impl Editor {
 
         if self.mode.is_full() {
             let show_inline_diagnostics = project_settings.diagnostics.inline.enabled;
-            let inline_blame_enabled = project_settings.git.inline_blame_enabled();
+            let inline_blame_enabled = project_settings.git.inline_blame.enabled;
             if self.show_inline_diagnostics != show_inline_diagnostics {
                 self.show_inline_diagnostics = show_inline_diagnostics;
                 self.refresh_inline_diagnostics(false, window, cx);
@@ -21679,11 +21678,12 @@ impl Editor {
     }
 }
 
+// todo(settings_refactor) this should not be!
 fn vim_enabled(cx: &App) -> bool {
     cx.global::<SettingsStore>()
         .raw_user_settings()
-        .get("vim_mode")
-        == Some(&serde_json::Value::Bool(true))
+        .and_then(|settings| settings.content.vim_mode)
+        == Some(true)
 }
 
 fn process_completion_for_edit(
@@ -23123,7 +23123,7 @@ impl EditorSnapshot {
         let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| {
             matches!(
                 ProjectSettings::get_global(cx).git.git_gutter,
-                Some(GitGutterSetting::TrackedFiles)
+                GitGutterSetting::TrackedFiles
             )
         });
         let gutter_settings = EditorSettings::get_global(cx).gutter;

crates/editor/src/editor_settings.rs πŸ”—

@@ -4,15 +4,19 @@ use std::num::NonZeroU32;
 use gpui::App;
 use language::CursorShape;
 use project::project_settings::DiagnosticSeverity;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi, VsCodeSettings};
+pub use settings::{
+    CurrentLineHighlight, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer,
+    GoToDefinitionFallback, HideMouseMode, MinimapThumb, MinimapThumbBorder, MultiCursorModifier,
+    ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder,
+    VsCodeSettings,
+};
+use settings::{Settings, SettingsContent};
 use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
-use util::serde::default_true;
+use util::MergeFrom;
 
 /// Imports from the VSCode settings at
 /// https://code.visualstudio.com/docs/reference/default-settings
-#[derive(Deserialize, Clone)]
+#[derive(Clone)]
 pub struct EditorSettings {
     pub cursor_blink: bool,
     pub cursor_shape: Option<CursorShape>,
@@ -41,83 +45,22 @@ pub struct EditorSettings {
     pub expand_excerpt_lines: u32,
     pub excerpt_context_lines: u32,
     pub middle_click_paste: bool,
-    #[serde(default)]
     pub double_click_in_multibuffer: DoubleClickInMultibuffer,
     pub search_wrap: bool,
-    #[serde(default)]
     pub search: SearchSettings,
     pub auto_signature_help: bool,
     pub show_signature_help_after_edits: bool,
-    #[serde(default)]
     pub go_to_definition_fallback: GoToDefinitionFallback,
     pub jupyter: Jupyter,
     pub hide_mouse: Option<HideMouseMode>,
     pub snippet_sort_order: SnippetSortOrder,
-    #[serde(default)]
     pub diagnostics_max_severity: Option<DiagnosticSeverity>,
     pub inline_code_actions: bool,
     pub drag_and_drop_selection: DragAndDropSelection,
     pub lsp_document_colors: DocumentColorsRenderMode,
     pub minimum_contrast_for_highlights: f32,
 }
-
-/// How to render LSP `textDocument/documentColor` colors in the editor.
-#[derive(
-    Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
-pub enum DocumentColorsRenderMode {
-    /// Do not query and render document colors.
-    None,
-    /// Render document colors as inlay hints near the color text.
-    #[default]
-    Inlay,
-    /// Draw a border around the color text.
-    Border,
-    /// Draw a background behind the color text.
-    Background,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-pub enum CurrentLineHighlight {
-    // Don't highlight the current line.
-    None,
-    // Highlight the gutter area.
-    Gutter,
-    // Highlight the editor area.
-    Line,
-    // Highlight the full line.
-    All,
-}
-
-/// When to populate a new search's query based on the text under the cursor.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-pub enum SeedQuerySetting {
-    /// Always populate the search query with the word under the cursor.
-    Always,
-    /// Only populate the search query when there is text selected.
-    Selection,
-    /// Never populate the search query
-    Never,
-}
-
-/// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers).
-#[derive(
-    Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
-pub enum DoubleClickInMultibuffer {
-    /// Behave as a regular buffer and select the whole word.
-    #[default]
-    Select,
-    /// Open the excerpt clicked as a new buffer in the new tab, if no `alt` modifier was pressed during double click.
-    /// Otherwise, behave as a regular buffer and select the whole word.
-    Open,
-}
-
-#[derive(Debug, Clone, Deserialize)]
+#[derive(Debug, Clone)]
 pub struct Jupyter {
     /// Whether the Jupyter feature is enabled.
     ///
@@ -125,18 +68,7 @@ pub struct Jupyter {
     pub enabled: bool,
 }
 
-#[derive(
-    Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
-pub struct JupyterContent {
-    /// Whether the Jupyter feature is enabled.
-    ///
-    /// Default: true
-    pub enabled: Option<bool>,
-}
-
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct StatusBar {
     /// Whether to display the active language button in the status bar.
     ///
@@ -148,7 +80,7 @@ pub struct StatusBar {
     pub cursor_position_button: bool,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Toolbar {
     pub breadcrumbs: bool,
     pub quick_actions: bool,
@@ -157,7 +89,7 @@ pub struct Toolbar {
     pub code_actions: bool,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub struct Scrollbar {
     pub show: ShowScrollbar,
     pub git_diff: bool,
@@ -169,7 +101,7 @@ pub struct Scrollbar {
     pub axes: ScrollbarAxes,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Copy, Clone, Debug, PartialEq)]
 pub struct Minimap {
     pub show: ShowMinimap,
     pub display_in: DisplayIn,
@@ -197,7 +129,7 @@ impl Minimap {
     }
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub struct Gutter {
     pub min_line_number_digits: usize,
     pub line_numbers: bool,
@@ -206,69 +138,8 @@ pub struct Gutter {
     pub folds: bool,
 }
 
-/// When to show the minimap in the editor.
-///
-/// Default: never
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowMinimap {
-    /// Follow the visibility of the scrollbar.
-    Auto,
-    /// Always show the minimap.
-    Always,
-    /// Never show the minimap.
-    #[default]
-    Never,
-}
-
-/// Where to show the minimap in the editor.
-///
-/// Default: all_editors
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum DisplayIn {
-    /// Show on all open editors.
-    AllEditors,
-    /// Show the minimap on the active editor only.
-    #[default]
-    ActiveEditor,
-}
-
-/// When to show the minimap thumb.
-///
-/// Default: always
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum MinimapThumb {
-    /// Show the minimap thumb only when the mouse is hovering over the minimap.
-    Hover,
-    /// Always show the minimap thumb.
-    #[default]
-    Always,
-}
-
-/// Defines the border style for the minimap's scrollbar thumb.
-///
-/// Default: left_open
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum MinimapThumbBorder {
-    /// Displays a border on all sides of the thumb.
-    Full,
-    /// Displays a border on all sides except the left side of the thumb.
-    #[default]
-    LeftOpen,
-    /// Displays a border on all sides except the right side of the thumb.
-    RightOpen,
-    /// Displays a border only on the left side of the thumb.
-    LeftOnly,
-    /// Displays the thumb without any border.
-    None,
-}
-
 /// Forcefully enable or disable the scrollbar for each axis
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "lowercase")]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub struct ScrollbarAxes {
     /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
     ///
@@ -282,480 +153,30 @@ pub struct ScrollbarAxes {
 }
 
 /// Whether to allow drag and drop text selection in buffer.
-#[derive(
-    Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
-)]
+#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
 pub struct DragAndDropSelection {
     /// When true, enables drag and drop text selection in buffer.
     ///
     /// Default: true
-    #[serde(default = "default_true")]
     pub enabled: bool,
 
     /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created.
     ///
     /// Default: 300
-    #[serde(default = "default_drag_and_drop_selection_delay_ms")]
     pub delay: u64,
 }
 
-fn default_drag_and_drop_selection_delay_ms() -> u64 {
-    300
-}
-
-/// Which diagnostic indicators to show in the scrollbar.
-///
-/// Default: all
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "lowercase")]
-pub enum ScrollbarDiagnostics {
-    /// Show all diagnostic levels: hint, information, warnings, error.
-    All,
-    /// Show only the following diagnostic levels: information, warning, error.
-    Information,
-    /// Show only the following diagnostic levels: warning, error.
-    Warning,
-    /// Show only the following diagnostic level: error.
-    Error,
-    /// Do not show diagnostics.
-    None,
-}
-
-/// The key to use for adding multiple cursors
-///
-/// Default: alt
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-pub enum MultiCursorModifier {
-    Alt,
-    #[serde(alias = "cmd", alias = "ctrl")]
-    CmdOrCtrl,
-}
-
-/// Whether the editor will scroll beyond the last line.
-///
-/// Default: one_page
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-pub enum ScrollBeyondLastLine {
-    /// The editor will not scroll beyond the last line.
-    Off,
-
-    /// The editor will scroll beyond the last line by one page.
-    OnePage,
-
-    /// The editor will scroll beyond the last line by the same number of lines as vertical_scroll_margin.
-    VerticalScrollMargin,
-}
-
 /// Default options for buffer and project search items.
-#[derive(
-    Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
-)]
+#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
 pub struct SearchSettings {
     /// Whether to show the project search button in the status bar.
-    #[serde(default = "default_true")]
     pub button: bool,
-    #[serde(default)]
     pub whole_word: bool,
-    #[serde(default)]
     pub case_sensitive: bool,
-    #[serde(default)]
     pub include_ignored: bool,
-    #[serde(default)]
     pub regex: bool,
 }
 
-/// What to do when go to definition yields no results.
-#[derive(
-    Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
-pub enum GoToDefinitionFallback {
-    /// Disables the fallback.
-    None,
-    /// Looks up references of the same symbol instead.
-    #[default]
-    FindAllReferences,
-}
-
-/// Determines when the mouse cursor should be hidden in an editor or input box.
-///
-/// Default: on_typing_and_movement
-#[derive(
-    Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
-pub enum HideMouseMode {
-    /// Never hide the mouse cursor
-    Never,
-    /// Hide only when typing
-    OnTyping,
-    /// Hide on both typing and cursor movement
-    #[default]
-    OnTypingAndMovement,
-}
-
-/// Determines how snippets are sorted relative to other completion items.
-///
-/// Default: inline
-#[derive(
-    Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
-pub enum SnippetSortOrder {
-    /// Place snippets at the top of the completion list
-    Top,
-    /// Sort snippets normally using the default comparison logic
-    #[default]
-    Inline,
-    /// Place snippets at the bottom of the completion list
-    Bottom,
-    /// Do not show snippets in the completion list
-    None,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_ui(group = "Editor")]
-#[settings_key(None)]
-pub struct EditorSettingsContent {
-    /// Whether the cursor blinks in the editor.
-    ///
-    /// Default: true
-    pub cursor_blink: Option<bool>,
-    /// Cursor shape for the default editor.
-    /// Can be "bar", "block", "underline", or "hollow".
-    ///
-    /// Default: bar
-    pub cursor_shape: Option<CursorShape>,
-    /// Determines when the mouse cursor should be hidden in an editor or input box.
-    ///
-    /// Default: on_typing_and_movement
-    pub hide_mouse: Option<HideMouseMode>,
-    /// Determines how snippets are sorted relative to other completion items.
-    ///
-    /// Default: inline
-    pub snippet_sort_order: Option<SnippetSortOrder>,
-    /// How to highlight the current line in the editor.
-    ///
-    /// Default: all
-    pub current_line_highlight: Option<CurrentLineHighlight>,
-    /// Whether to highlight all occurrences of the selected text in an editor.
-    ///
-    /// Default: true
-    pub selection_highlight: Option<bool>,
-    /// Whether the text selection should have rounded corners.
-    ///
-    /// Default: true
-    pub rounded_selection: Option<bool>,
-    /// The debounce delay before querying highlights from the language
-    /// server based on the current cursor location.
-    ///
-    /// Default: 75
-    pub lsp_highlight_debounce: Option<u64>,
-    /// Whether to show the informational hover box when moving the mouse
-    /// over symbols in the editor.
-    ///
-    /// Default: true
-    pub hover_popover_enabled: Option<bool>,
-    /// Time to wait in milliseconds before showing the informational hover box.
-    ///
-    /// Default: 300
-    pub hover_popover_delay: Option<u64>,
-    /// Status bar related settings
-    pub status_bar: Option<StatusBarContent>,
-    /// Toolbar related settings
-    pub toolbar: Option<ToolbarContent>,
-    /// Scrollbar related settings
-    pub scrollbar: Option<ScrollbarContent>,
-    /// Minimap related settings
-    pub minimap: Option<MinimapContent>,
-    /// Gutter related settings
-    pub gutter: Option<GutterContent>,
-    /// Whether the editor will scroll beyond the last line.
-    ///
-    /// Default: one_page
-    pub scroll_beyond_last_line: Option<ScrollBeyondLastLine>,
-    /// The number of lines to keep above/below the cursor when auto-scrolling.
-    ///
-    /// Default: 3.
-    pub vertical_scroll_margin: Option<f32>,
-    /// Whether to scroll when clicking near the edge of the visible text area.
-    ///
-    /// Default: false
-    pub autoscroll_on_clicks: Option<bool>,
-    /// The number of characters to keep on either side when scrolling with the mouse.
-    ///
-    /// Default: 5.
-    pub horizontal_scroll_margin: Option<f32>,
-    /// Scroll sensitivity multiplier. This multiplier is applied
-    /// to both the horizontal and vertical delta values while scrolling.
-    ///
-    /// Default: 1.0
-    pub scroll_sensitivity: Option<f32>,
-    /// Scroll sensitivity multiplier for fast scrolling. This multiplier is applied
-    /// to both the horizontal and vertical delta values while scrolling. Fast scrolling
-    /// happens when a user holds the alt or option key while scrolling.
-    ///
-    /// Default: 4.0
-    pub fast_scroll_sensitivity: Option<f32>,
-    /// Whether the line numbers on editors gutter are relative or not.
-    ///
-    /// Default: false
-    pub relative_line_numbers: Option<bool>,
-    /// When to populate a new search's query based on the text under the cursor.
-    ///
-    /// Default: always
-    pub seed_search_query_from_cursor: Option<SeedQuerySetting>,
-    pub use_smartcase_search: Option<bool>,
-    /// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
-    ///
-    /// Default: alt
-    pub multi_cursor_modifier: Option<MultiCursorModifier>,
-    /// Hide the values of variables in `private` files, as defined by the
-    /// private_files setting. This only changes the visual representation,
-    /// the values are still present in the file and can be selected / copied / pasted
-    ///
-    /// Default: false
-    pub redact_private_values: Option<bool>,
-
-    /// How many lines to expand the multibuffer excerpts by default
-    ///
-    /// Default: 3
-    pub expand_excerpt_lines: Option<u32>,
-
-    /// How many lines of context to provide in multibuffer excerpts by default
-    ///
-    /// Default: 2
-    pub excerpt_context_lines: Option<u32>,
-
-    /// Whether to enable middle-click paste on Linux
-    ///
-    /// Default: true
-    pub middle_click_paste: Option<bool>,
-
-    /// What to do when multibuffer is double clicked in some of its excerpts
-    /// (parts of singleton buffers).
-    ///
-    /// Default: select
-    pub double_click_in_multibuffer: Option<DoubleClickInMultibuffer>,
-    /// Whether the editor search results will loop
-    ///
-    /// Default: true
-    pub search_wrap: Option<bool>,
-
-    /// Defaults to use when opening a new buffer and project search items.
-    ///
-    /// Default: nothing is enabled
-    pub search: Option<SearchSettings>,
-
-    /// Whether to automatically show a signature help pop-up or not.
-    ///
-    /// Default: false
-    pub auto_signature_help: Option<bool>,
-
-    /// Whether to show the signature help pop-up after completions or bracket pairs inserted.
-    ///
-    /// Default: false
-    pub show_signature_help_after_edits: Option<bool>,
-    /// The minimum APCA perceptual contrast to maintain when
-    /// rendering text over highlight backgrounds in the editor.
-    ///
-    /// Values range from 0 to 106. Set to 0 to disable adjustments.
-    /// Default: 45
-    pub minimum_contrast_for_highlights: Option<f32>,
-
-    /// Whether to follow-up empty go to definition responses from the language server or not.
-    /// `FindAllReferences` allows to look up references of the same symbol instead.
-    /// `None` disables the fallback.
-    ///
-    /// Default: FindAllReferences
-    pub go_to_definition_fallback: Option<GoToDefinitionFallback>,
-
-    /// Jupyter REPL settings.
-    pub jupyter: Option<JupyterContent>,
-
-    /// Which level to use to filter out diagnostics displayed in the editor.
-    ///
-    /// Affects the editor rendering only, and does not interrupt
-    /// the functionality of diagnostics fetching and project diagnostics editor.
-    /// Which files containing diagnostic errors/warnings to mark in the tabs.
-    /// Diagnostics are only shown when file icons are also active.
-    ///
-    /// Shows all diagnostics if not specified.
-    ///
-    /// Default: warning
-    #[serde(default)]
-    pub diagnostics_max_severity: Option<DiagnosticSeverity>,
-
-    /// Whether to show code action button at start of buffer line.
-    ///
-    /// Default: true
-    pub inline_code_actions: Option<bool>,
-
-    /// Drag and drop related settings
-    pub drag_and_drop_selection: Option<DragAndDropSelection>,
-
-    /// How to render LSP `textDocument/documentColor` colors in the editor.
-    ///
-    /// Default: [`DocumentColorsRenderMode::Inlay`]
-    pub lsp_document_colors: Option<DocumentColorsRenderMode>,
-}
-
-// Status bar related settings
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
-pub struct StatusBarContent {
-    /// Whether to display the active language button in the status bar.
-    ///
-    /// Default: true
-    pub active_language_button: Option<bool>,
-    /// Whether to show the cursor position button in the status bar.
-    ///
-    /// Default: true
-    pub cursor_position_button: Option<bool>,
-}
-
-// Toolbar related settings
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
-pub struct ToolbarContent {
-    /// Whether to display breadcrumbs in the editor toolbar.
-    ///
-    /// Default: true
-    pub breadcrumbs: Option<bool>,
-    /// Whether to display quick action buttons in the editor toolbar.
-    ///
-    /// Default: true
-    pub quick_actions: Option<bool>,
-    /// Whether to show the selections menu in the editor toolbar.
-    ///
-    /// Default: true
-    pub selections_menu: Option<bool>,
-    /// Whether to display Agent review buttons in the editor toolbar.
-    /// Only applicable while reviewing a file edited by the Agent.
-    ///
-    /// Default: true
-    pub agent_review: Option<bool>,
-    /// Whether to display code action buttons in the editor toolbar.
-    ///
-    /// Default: false
-    pub code_actions: Option<bool>,
-}
-
-/// Scrollbar related settings
-#[derive(
-    Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default, SettingsUi,
-)]
-pub struct ScrollbarContent {
-    /// When to show the scrollbar in the editor.
-    ///
-    /// Default: auto
-    pub show: Option<ShowScrollbar>,
-    /// Whether to show git diff indicators in the scrollbar.
-    ///
-    /// Default: true
-    pub git_diff: Option<bool>,
-    /// Whether to show buffer search result indicators in the scrollbar.
-    ///
-    /// Default: true
-    pub search_results: Option<bool>,
-    /// Whether to show selected text occurrences in the scrollbar.
-    ///
-    /// Default: true
-    pub selected_text: Option<bool>,
-    /// Whether to show selected symbol occurrences in the scrollbar.
-    ///
-    /// Default: true
-    pub selected_symbol: Option<bool>,
-    /// Which diagnostic indicators to show in the scrollbar:
-    ///
-    /// Default: all
-    pub diagnostics: Option<ScrollbarDiagnostics>,
-    /// Whether to show cursor positions in the scrollbar.
-    ///
-    /// Default: true
-    pub cursors: Option<bool>,
-    /// Forcefully enable or disable the scrollbar for each axis
-    pub axes: Option<ScrollbarAxesContent>,
-}
-
-/// Minimap related settings
-#[derive(
-    Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi,
-)]
-pub struct MinimapContent {
-    /// When to show the minimap in the editor.
-    ///
-    /// Default: never
-    pub show: Option<ShowMinimap>,
-
-    /// Where to show the minimap in the editor.
-    ///
-    /// Default: [`DisplayIn::ActiveEditor`]
-    pub display_in: Option<DisplayIn>,
-
-    /// When to show the minimap thumb.
-    ///
-    /// Default: always
-    pub thumb: Option<MinimapThumb>,
-
-    /// Defines the border style for the minimap's scrollbar thumb.
-    ///
-    /// Default: left_open
-    pub thumb_border: Option<MinimapThumbBorder>,
-
-    /// How to highlight the current line in the minimap.
-    ///
-    /// Default: inherits editor line highlights setting
-    pub current_line_highlight: Option<Option<CurrentLineHighlight>>,
-
-    /// Maximum number of columns to display in the minimap.
-    ///
-    /// Default: 80
-    pub max_width_columns: Option<num::NonZeroU32>,
-}
-
-/// Forcefully enable or disable the scrollbar for each axis
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
-pub struct ScrollbarAxesContent {
-    /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
-    ///
-    /// Default: true
-    horizontal: Option<bool>,
-
-    /// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
-    ///
-    /// Default: true
-    vertical: Option<bool>,
-}
-
-/// Gutter related settings
-#[derive(
-    Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
-)]
-#[settings_ui(group = "Gutter")]
-pub struct GutterContent {
-    /// Whether to show line numbers in the gutter.
-    ///
-    /// Default: true
-    pub line_numbers: Option<bool>,
-    /// Minimum number of characters to reserve space for in the gutter.
-    ///
-    /// Default: 4
-    pub min_line_number_digits: Option<usize>,
-    /// Whether to show runnable buttons in the gutter.
-    ///
-    /// Default: true
-    pub runnables: Option<bool>,
-    /// Whether to show breakpoints in the gutter.
-    ///
-    /// Default: true
-    pub breakpoints: Option<bool>,
-    /// Whether to show fold buttons in the gutter.
-    ///
-    /// Default: true
-    pub folds: Option<bool>,
-}
-
 impl EditorSettings {
     pub fn jupyter_enabled(cx: &App) -> bool {
         EditorSettings::get_global(cx).jupyter.enabled
@@ -769,16 +190,266 @@ impl ScrollbarVisibility for EditorSettings {
 }
 
 impl Settings for EditorSettings {
-    type FileContent = EditorSettingsContent;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let editor = content.editor.clone();
+        let scrollbar = editor.scrollbar.unwrap();
+        let minimap = editor.minimap.unwrap();
+        let gutter = editor.gutter.unwrap();
+        let axes = scrollbar.axes.unwrap();
+        let status_bar = editor.status_bar.unwrap();
+        let toolbar = editor.toolbar.unwrap();
+        let search = editor.search.unwrap();
+        let drag_and_drop_selection = editor.drag_and_drop_selection.unwrap();
+        Self {
+            cursor_blink: editor.cursor_blink.unwrap(),
+            cursor_shape: editor.cursor_shape.map(Into::into),
+            current_line_highlight: editor.current_line_highlight.unwrap(),
+            selection_highlight: editor.selection_highlight.unwrap(),
+            rounded_selection: editor.rounded_selection.unwrap(),
+            lsp_highlight_debounce: editor.lsp_highlight_debounce.unwrap(),
+            hover_popover_enabled: editor.hover_popover_enabled.unwrap(),
+            hover_popover_delay: editor.hover_popover_delay.unwrap(),
+            status_bar: StatusBar {
+                active_language_button: status_bar.active_language_button.unwrap(),
+                cursor_position_button: status_bar.cursor_position_button.unwrap(),
+            },
+            toolbar: Toolbar {
+                breadcrumbs: toolbar.breadcrumbs.unwrap(),
+                quick_actions: toolbar.quick_actions.unwrap(),
+                selections_menu: toolbar.selections_menu.unwrap(),
+                agent_review: toolbar.agent_review.unwrap(),
+                code_actions: toolbar.code_actions.unwrap(),
+            },
+            scrollbar: Scrollbar {
+                show: scrollbar.show.map(Into::into).unwrap(),
+                git_diff: scrollbar.git_diff.unwrap(),
+                selected_text: scrollbar.selected_text.unwrap(),
+                selected_symbol: scrollbar.selected_symbol.unwrap(),
+                search_results: scrollbar.search_results.unwrap(),
+                diagnostics: scrollbar.diagnostics.unwrap(),
+                cursors: scrollbar.cursors.unwrap(),
+                axes: ScrollbarAxes {
+                    horizontal: axes.horizontal.unwrap(),
+                    vertical: axes.vertical.unwrap(),
+                },
+            },
+            minimap: Minimap {
+                show: minimap.show.unwrap(),
+                display_in: minimap.display_in.unwrap(),
+                thumb: minimap.thumb.unwrap(),
+                thumb_border: minimap.thumb_border.unwrap(),
+                current_line_highlight: minimap.current_line_highlight.flatten(),
+                max_width_columns: minimap.max_width_columns.unwrap(),
+            },
+            gutter: Gutter {
+                min_line_number_digits: gutter.min_line_number_digits.unwrap(),
+                line_numbers: gutter.line_numbers.unwrap(),
+                runnables: gutter.runnables.unwrap(),
+                breakpoints: gutter.breakpoints.unwrap(),
+                folds: gutter.folds.unwrap(),
+            },
+            scroll_beyond_last_line: editor.scroll_beyond_last_line.unwrap(),
+            vertical_scroll_margin: editor.vertical_scroll_margin.unwrap(),
+            autoscroll_on_clicks: editor.autoscroll_on_clicks.unwrap(),
+            horizontal_scroll_margin: editor.horizontal_scroll_margin.unwrap(),
+            scroll_sensitivity: editor.scroll_sensitivity.unwrap(),
+            fast_scroll_sensitivity: editor.fast_scroll_sensitivity.unwrap(),
+            relative_line_numbers: editor.relative_line_numbers.unwrap(),
+            seed_search_query_from_cursor: editor.seed_search_query_from_cursor.unwrap(),
+            use_smartcase_search: editor.use_smartcase_search.unwrap(),
+            multi_cursor_modifier: editor.multi_cursor_modifier.unwrap(),
+            redact_private_values: editor.redact_private_values.unwrap(),
+            expand_excerpt_lines: editor.expand_excerpt_lines.unwrap(),
+            excerpt_context_lines: editor.excerpt_context_lines.unwrap(),
+            middle_click_paste: editor.middle_click_paste.unwrap(),
+            double_click_in_multibuffer: editor.double_click_in_multibuffer.unwrap(),
+            search_wrap: editor.search_wrap.unwrap(),
+            search: SearchSettings {
+                button: search.button.unwrap(),
+                whole_word: search.whole_word.unwrap(),
+                case_sensitive: search.case_sensitive.unwrap(),
+                include_ignored: search.include_ignored.unwrap(),
+                regex: search.regex.unwrap(),
+            },
+            auto_signature_help: editor.auto_signature_help.unwrap(),
+            show_signature_help_after_edits: editor.show_signature_help_after_edits.unwrap(),
+            go_to_definition_fallback: editor.go_to_definition_fallback.unwrap(),
+            jupyter: Jupyter {
+                enabled: editor.jupyter.unwrap().enabled.unwrap(),
+            },
+            hide_mouse: editor.hide_mouse,
+            snippet_sort_order: editor.snippet_sort_order.unwrap(),
+            diagnostics_max_severity: editor.diagnostics_max_severity.map(Into::into),
+            inline_code_actions: editor.inline_code_actions.unwrap(),
+            drag_and_drop_selection: DragAndDropSelection {
+                enabled: drag_and_drop_selection.enabled.unwrap(),
+                delay: drag_and_drop_selection.delay.unwrap(),
+            },
+            lsp_document_colors: editor.lsp_document_colors.unwrap(),
+            minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap(),
+        }
+    }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let editor = &content.editor;
+        self.cursor_blink.merge_from(&editor.cursor_blink);
+        if let Some(cursor_shape) = editor.cursor_shape {
+            self.cursor_shape = Some(cursor_shape.into())
+        }
+        self.current_line_highlight
+            .merge_from(&editor.current_line_highlight);
+        self.selection_highlight
+            .merge_from(&editor.selection_highlight);
+        self.rounded_selection.merge_from(&editor.rounded_selection);
+        self.lsp_highlight_debounce
+            .merge_from(&editor.lsp_highlight_debounce);
+        self.hover_popover_enabled
+            .merge_from(&editor.hover_popover_enabled);
+        self.hover_popover_delay
+            .merge_from(&editor.hover_popover_delay);
+        self.scroll_beyond_last_line
+            .merge_from(&editor.scroll_beyond_last_line);
+        self.vertical_scroll_margin
+            .merge_from(&editor.vertical_scroll_margin);
+        self.autoscroll_on_clicks
+            .merge_from(&editor.autoscroll_on_clicks);
+        self.horizontal_scroll_margin
+            .merge_from(&editor.horizontal_scroll_margin);
+        self.scroll_sensitivity
+            .merge_from(&editor.scroll_sensitivity);
+        self.fast_scroll_sensitivity
+            .merge_from(&editor.fast_scroll_sensitivity);
+        self.relative_line_numbers
+            .merge_from(&editor.relative_line_numbers);
+        self.seed_search_query_from_cursor
+            .merge_from(&editor.seed_search_query_from_cursor);
+        self.use_smartcase_search
+            .merge_from(&editor.use_smartcase_search);
+        self.multi_cursor_modifier
+            .merge_from(&editor.multi_cursor_modifier);
+        self.redact_private_values
+            .merge_from(&editor.redact_private_values);
+        self.expand_excerpt_lines
+            .merge_from(&editor.expand_excerpt_lines);
+        self.excerpt_context_lines
+            .merge_from(&editor.excerpt_context_lines);
+        self.middle_click_paste
+            .merge_from(&editor.middle_click_paste);
+        self.double_click_in_multibuffer
+            .merge_from(&editor.double_click_in_multibuffer);
+        self.search_wrap.merge_from(&editor.search_wrap);
+        self.auto_signature_help
+            .merge_from(&editor.auto_signature_help);
+        self.show_signature_help_after_edits
+            .merge_from(&editor.show_signature_help_after_edits);
+        self.go_to_definition_fallback
+            .merge_from(&editor.go_to_definition_fallback);
+        if let Some(hide_mouse) = editor.hide_mouse {
+            self.hide_mouse = Some(hide_mouse)
+        }
+        self.snippet_sort_order
+            .merge_from(&editor.snippet_sort_order);
+        if let Some(diagnostics_max_severity) = editor.diagnostics_max_severity {
+            self.diagnostics_max_severity = Some(diagnostics_max_severity.into());
+        }
+        self.inline_code_actions
+            .merge_from(&editor.inline_code_actions);
+        self.lsp_document_colors
+            .merge_from(&editor.lsp_document_colors);
+        self.minimum_contrast_for_highlights
+            .merge_from(&editor.minimum_contrast_for_highlights);
+
+        if let Some(status_bar) = &editor.status_bar {
+            self.status_bar
+                .active_language_button
+                .merge_from(&status_bar.active_language_button);
+            self.status_bar
+                .cursor_position_button
+                .merge_from(&status_bar.cursor_position_button);
+        }
+        if let Some(toolbar) = &editor.toolbar {
+            self.toolbar.breadcrumbs.merge_from(&toolbar.breadcrumbs);
+            self.toolbar
+                .quick_actions
+                .merge_from(&toolbar.quick_actions);
+            self.toolbar
+                .selections_menu
+                .merge_from(&toolbar.selections_menu);
+            self.toolbar.agent_review.merge_from(&toolbar.agent_review);
+            self.toolbar.code_actions.merge_from(&toolbar.code_actions);
+        }
+        if let Some(scrollbar) = &editor.scrollbar {
+            self.scrollbar
+                .show
+                .merge_from(&scrollbar.show.map(Into::into));
+            self.scrollbar.git_diff.merge_from(&scrollbar.git_diff);
+            self.scrollbar
+                .selected_text
+                .merge_from(&scrollbar.selected_text);
+            self.scrollbar
+                .selected_symbol
+                .merge_from(&scrollbar.selected_symbol);
+            self.scrollbar
+                .search_results
+                .merge_from(&scrollbar.search_results);
+            self.scrollbar
+                .diagnostics
+                .merge_from(&scrollbar.diagnostics);
+            self.scrollbar.cursors.merge_from(&scrollbar.cursors);
+            if let Some(axes) = &scrollbar.axes {
+                self.scrollbar.axes.horizontal.merge_from(&axes.horizontal);
+                self.scrollbar.axes.vertical.merge_from(&axes.vertical);
+            }
+        }
+        if let Some(minimap) = &editor.minimap {
+            self.minimap.show.merge_from(&minimap.show);
+            self.minimap.display_in.merge_from(&minimap.display_in);
+            self.minimap.thumb.merge_from(&minimap.thumb);
+            self.minimap.thumb_border.merge_from(&minimap.thumb_border);
+            self.minimap
+                .current_line_highlight
+                .merge_from(&minimap.current_line_highlight);
+            self.minimap
+                .max_width_columns
+                .merge_from(&minimap.max_width_columns);
+        }
+        if let Some(gutter) = &editor.gutter {
+            self.gutter
+                .min_line_number_digits
+                .merge_from(&gutter.min_line_number_digits);
+            self.gutter.line_numbers.merge_from(&gutter.line_numbers);
+            self.gutter.runnables.merge_from(&gutter.runnables);
+            self.gutter.breakpoints.merge_from(&gutter.breakpoints);
+            self.gutter.folds.merge_from(&gutter.folds);
+        }
+        if let Some(search) = &editor.search {
+            self.search.button.merge_from(&search.button);
+            self.search.whole_word.merge_from(&search.whole_word);
+            self.search
+                .case_sensitive
+                .merge_from(&search.case_sensitive);
+            self.search
+                .include_ignored
+                .merge_from(&search.include_ignored);
+            self.search.regex.merge_from(&search.regex);
+        }
+        if let Some(enabled) = editor.jupyter.as_ref().and_then(|jupyter| jupyter.enabled) {
+            self.jupyter.enabled = enabled;
+        }
+        if let Some(drag_and_drop_selection) = &editor.drag_and_drop_selection {
+            self.drag_and_drop_selection
+                .enabled
+                .merge_from(&drag_and_drop_selection.enabled);
+            self.drag_and_drop_selection
+                .delay
+                .merge_from(&drag_and_drop_selection.delay);
+        }
     }
 
-    fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent) {
+    fn import_from_vscode(vscode: &VsCodeSettings, current: &mut SettingsContent) {
         vscode.enum_setting(
             "editor.cursorBlinking",
-            &mut current.cursor_blink,
+            &mut current.editor.cursor_blink,
             |s| match s {
                 "blink" | "phase" | "expand" | "smooth" => Some(true),
                 "solid" => Some(false),
@@ -787,19 +458,19 @@ impl Settings for EditorSettings {
         );
         vscode.enum_setting(
             "editor.cursorStyle",
-            &mut current.cursor_shape,
+            &mut current.editor.cursor_shape,
             |s| match s {
-                "block" => Some(CursorShape::Block),
-                "block-outline" => Some(CursorShape::Hollow),
-                "line" | "line-thin" => Some(CursorShape::Bar),
-                "underline" | "underline-thin" => Some(CursorShape::Underline),
+                "block" => Some(settings::CursorShape::Block),
+                "block-outline" => Some(settings::CursorShape::Hollow),
+                "line" | "line-thin" => Some(settings::CursorShape::Bar),
+                "underline" | "underline-thin" => Some(settings::CursorShape::Underline),
                 _ => None,
             },
         );
 
         vscode.enum_setting(
             "editor.renderLineHighlight",
-            &mut current.current_line_highlight,
+            &mut current.editor.current_line_highlight,
             |s| match s {
                 "gutter" => Some(CurrentLineHighlight::Gutter),
                 "line" => Some(CurrentLineHighlight::Line),

crates/editor/src/editor_settings_controls.rs πŸ”—

@@ -1,8 +1,8 @@
 use std::sync::Arc;
 
 use gpui::{App, FontFeatures, FontWeight};
-use project::project_settings::{InlineBlameSettings, ProjectSettings};
-use settings::{EditableSettingControl, Settings};
+use project::project_settings::ProjectSettings;
+use settings::{EditableSettingControl, Settings, SettingsContent};
 use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
 use ui::{
     CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
@@ -59,7 +59,6 @@ struct BufferFontFamilyControl;
 
 impl EditableSettingControl for BufferFontFamilyControl {
     type Value = SharedString;
-    type Settings = ThemeSettings;
 
     fn name(&self) -> SharedString {
         "Buffer Font Family".into()
@@ -70,12 +69,8 @@ impl EditableSettingControl for BufferFontFamilyControl {
         settings.buffer_font.family.clone()
     }
 
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        settings.buffer_font_family = Some(FontFamilyName(value.into()));
+    fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
+        settings.theme.buffer_font_family = Some(FontFamilyName(value.into()));
     }
 }
 
@@ -118,7 +113,6 @@ struct BufferFontSizeControl;
 
 impl EditableSettingControl for BufferFontSizeControl {
     type Value = Pixels;
-    type Settings = ThemeSettings;
 
     fn name(&self) -> SharedString {
         "Buffer Font Size".into()
@@ -128,12 +122,8 @@ impl EditableSettingControl for BufferFontSizeControl {
         ThemeSettings::get_global(cx).buffer_font_size(cx)
     }
 
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        settings.buffer_font_size = Some(value.into());
+    fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
+        settings.theme.buffer_font_size = Some(value.into());
     }
 }
 
@@ -162,7 +152,6 @@ struct BufferFontWeightControl;
 
 impl EditableSettingControl for BufferFontWeightControl {
     type Value = FontWeight;
-    type Settings = ThemeSettings;
 
     fn name(&self) -> SharedString {
         "Buffer Font Weight".into()
@@ -173,12 +162,8 @@ impl EditableSettingControl for BufferFontWeightControl {
         settings.buffer_font.weight
     }
 
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        settings.buffer_font_weight = Some(value.0);
+    fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
+        settings.theme.buffer_font_weight = Some(value.0);
     }
 }
 
@@ -215,7 +200,6 @@ struct BufferFontLigaturesControl;
 
 impl EditableSettingControl for BufferFontLigaturesControl {
     type Value = bool;
-    type Settings = ThemeSettings;
 
     fn name(&self) -> SharedString {
         "Buffer Font Ligatures".into()
@@ -230,14 +214,11 @@ impl EditableSettingControl for BufferFontLigaturesControl {
             .unwrap_or(true)
     }
 
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
+    fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
         let value = if value { 1 } else { 0 };
 
         let mut features = settings
+            .theme
             .buffer_font_features
             .as_ref()
             .map(|features| features.tag_value_list().to_vec())
@@ -249,7 +230,7 @@ impl EditableSettingControl for BufferFontLigaturesControl {
             features.push(("calt".into(), value));
         }
 
-        settings.buffer_font_features = Some(FontFeatures(Arc::new(features)));
+        settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features)));
     }
 }
 
@@ -279,7 +260,6 @@ struct InlineGitBlameControl;
 
 impl EditableSettingControl for InlineGitBlameControl {
     type Value = bool;
-    type Settings = ProjectSettings;
 
     fn name(&self) -> SharedString {
         "Inline Git Blame".into()
@@ -287,22 +267,16 @@ impl EditableSettingControl for InlineGitBlameControl {
 
     fn read(cx: &App) -> Self::Value {
         let settings = ProjectSettings::get_global(cx);
-        settings.git.inline_blame_enabled()
+        settings.git.inline_blame.enabled
     }
 
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        if let Some(inline_blame) = settings.git.inline_blame.as_mut() {
-            inline_blame.enabled = value;
-        } else {
-            settings.git.inline_blame = Some(InlineBlameSettings {
-                enabled: false,
-                ..Default::default()
-            });
-        }
+    fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
+        settings
+            .git
+            .get_or_insert_default()
+            .inline_blame
+            .get_or_insert_default()
+            .enabled = Some(value)
     }
 }
 
@@ -332,7 +306,6 @@ struct LineNumbersControl;
 
 impl EditableSettingControl for LineNumbersControl {
     type Value = bool;
-    type Settings = EditorSettings;
 
     fn name(&self) -> SharedString {
         "Line Numbers".into()
@@ -343,19 +316,8 @@ impl EditableSettingControl for LineNumbersControl {
         settings.gutter.line_numbers
     }
 
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        if let Some(gutter) = settings.gutter.as_mut() {
-            gutter.line_numbers = Some(value);
-        } else {
-            settings.gutter = Some(crate::editor_settings::GutterContent {
-                line_numbers: Some(value),
-                ..Default::default()
-            });
-        }
+    fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
+        settings.editor.gutter.get_or_insert_default().line_numbers = Some(value);
     }
 }
 
@@ -385,7 +347,6 @@ struct RelativeLineNumbersControl;
 
 impl EditableSettingControl for RelativeLineNumbersControl {
     type Value = bool;
-    type Settings = EditorSettings;
 
     fn name(&self) -> SharedString {
         "Relative Line Numbers".into()
@@ -396,12 +357,8 @@ impl EditableSettingControl for RelativeLineNumbersControl {
         settings.relative_line_numbers
     }
 
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        settings.relative_line_numbers = Some(value);
+    fn apply(settings: &mut SettingsContent, value: Self::Value, _cx: &App) {
+        settings.editor.relative_line_numbers = Some(value);
     }
 }
 

crates/editor/src/editor_tests.rs πŸ”—

@@ -22,15 +22,15 @@ use indoc::indoc;
 use language::{
     BracketPairConfig,
     Capability::ReadWrite,
-    DiagnosticSourceKind, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
-    LanguageName, Override, Point,
+    DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
+    LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
     language_settings::{
-        AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings, FormatterList,
-        LanguageSettingsContent, LspInsertMode, PrettierSettings, SelectedFormatter,
+        CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
+        SelectedFormatter,
     },
     tree_sitter_python,
 };
-use language_settings::{Formatter, IndentGuideSettings};
+use language_settings::Formatter;
 use lsp::CompletionParams;
 use multi_buffer::{IndentGuide, PathKey};
 use parking_lot::Mutex;
@@ -38,9 +38,13 @@ use pretty_assertions::{assert_eq, assert_ne};
 use project::{
     FakeFs,
     debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
-    project_settings::{LspSettings, ProjectSettings},
+    project_settings::LspSettings,
 };
 use serde_json::{self, json};
+use settings::{
+    AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
+    ProjectSettingsContent,
+};
 use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
 use std::{
     iter,
@@ -11699,10 +11703,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
     update_test_language_settings(cx, |settings| {
         // Enable Prettier formatting for the same buffer, and ensure
         // LSP is called instead of Prettier.
-        settings.defaults.prettier = Some(PrettierSettings {
-            allowed: true,
-            ..PrettierSettings::default()
-        });
+        settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
     });
     let mut fake_servers = language_registry.register_fake_lsp(
         "Rust",
@@ -12088,10 +12089,7 @@ async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
         Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
     )));
     update_test_language_settings(cx, |settings| {
-        settings.defaults.prettier = Some(PrettierSettings {
-            allowed: true,
-            ..PrettierSettings::default()
-        });
+        settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
     });
     let mut fake_servers = language_registry.register_fake_lsp(
         "TypeScript",
@@ -12402,8 +12400,8 @@ async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
 
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|settings, cx| {
-            settings.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.auto_signature_help = Some(true);
+            settings.update_user_settings(cx, |settings| {
+                settings.editor.auto_signature_help = Some(true);
             });
         });
     });
@@ -12542,9 +12540,9 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA
 
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|settings, cx| {
-            settings.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.auto_signature_help = Some(false);
-                settings.show_signature_help_after_edits = Some(false);
+            settings.update_user_settings(cx, |settings| {
+                settings.editor.auto_signature_help = Some(false);
+                settings.editor.show_signature_help_after_edits = Some(false);
             });
         });
     });
@@ -12669,9 +12667,9 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA
     // Ensure that signature_help is called when enabled afte edits
     cx.update(|_, cx| {
         cx.update_global::<SettingsStore, _>(|settings, cx| {
-            settings.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.auto_signature_help = Some(false);
-                settings.show_signature_help_after_edits = Some(true);
+            settings.update_user_settings(cx, |settings| {
+                settings.editor.auto_signature_help = Some(false);
+                settings.editor.show_signature_help_after_edits = Some(true);
             });
         });
     });
@@ -12711,9 +12709,9 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA
     // Ensure that signature_help is called when auto signature help override is enabled
     cx.update(|_, cx| {
         cx.update_global::<SettingsStore, _>(|settings, cx| {
-            settings.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.auto_signature_help = Some(true);
-                settings.show_signature_help_after_edits = Some(false);
+            settings.update_user_settings(cx, |settings| {
+                settings.editor.auto_signature_help = Some(true);
+                settings.editor.show_signature_help_after_edits = Some(false);
             });
         });
     });
@@ -12755,8 +12753,8 @@ async fn test_signature_help(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|settings, cx| {
-            settings.update_user_settings::<EditorSettings>(cx, |settings| {
-                settings.auto_signature_help = Some(true);
+            settings.update_user_settings(cx, |settings| {
+                settings.editor.auto_signature_help = Some(true);
             });
         });
     });
@@ -13346,12 +13344,11 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
             );
 
             update_test_language_settings(&mut cx, |settings| {
-                settings.defaults.completions = Some(CompletionSettings {
-                    lsp_insert_mode,
-                    words: WordsCompletionMode::Disabled,
-                    words_min_length: 0,
-                    lsp: true,
-                    lsp_fetch_timeout_ms: 0,
+                settings.defaults.completions = Some(CompletionSettingsContent {
+                    lsp_insert_mode: Some(lsp_insert_mode),
+                    words: Some(WordsCompletionMode::Disabled),
+                    words_min_length: Some(0),
+                    ..Default::default()
                 });
             });
 
@@ -13406,13 +13403,12 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
     let expected_with_replace_mode = "SubscriptionErrorˇ";
 
     update_test_language_settings(&mut cx, |settings| {
-        settings.defaults.completions = Some(CompletionSettings {
-            words: WordsCompletionMode::Disabled,
-            words_min_length: 0,
+        settings.defaults.completions = Some(CompletionSettingsContent {
+            words: Some(WordsCompletionMode::Disabled),
+            words_min_length: Some(0),
             // set the opposite here to ensure that the action is overriding the default behavior
-            lsp_insert_mode: LspInsertMode::Insert,
-            lsp: true,
-            lsp_fetch_timeout_ms: 0,
+            lsp_insert_mode: Some(LspInsertMode::Insert),
+            ..Default::default()
         });
     });
 
@@ -13443,13 +13439,12 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
     apply_additional_edits.await.unwrap();
 
     update_test_language_settings(&mut cx, |settings| {
-        settings.defaults.completions = Some(CompletionSettings {
-            words: WordsCompletionMode::Disabled,
-            words_min_length: 0,
+        settings.defaults.completions = Some(CompletionSettingsContent {
+            words: Some(WordsCompletionMode::Disabled),
+            words_min_length: Some(0),
             // set the opposite here to ensure that the action is overriding the default behavior
-            lsp_insert_mode: LspInsertMode::Replace,
-            lsp: true,
-            lsp_fetch_timeout_ms: 0,
+            lsp_insert_mode: Some(LspInsertMode::Replace),
+            ..Default::default()
         });
     });
 
@@ -14185,12 +14180,11 @@ async fn test_completion_reuse(cx: &mut TestAppContext) {
 async fn test_word_completion(cx: &mut TestAppContext) {
     let lsp_fetch_timeout_ms = 10;
     init_test(cx, |language_settings| {
-        language_settings.defaults.completions = Some(CompletionSettings {
-            words: WordsCompletionMode::Fallback,
-            words_min_length: 0,
-            lsp: true,
-            lsp_fetch_timeout_ms: 10,
-            lsp_insert_mode: LspInsertMode::Insert,
+        language_settings.defaults.completions = Some(CompletionSettingsContent {
+            words_min_length: Some(0),
+            lsp_fetch_timeout_ms: Some(10),
+            lsp_insert_mode: Some(LspInsertMode::Insert),
+            ..Default::default()
         });
     });
 
@@ -14282,12 +14276,11 @@ async fn test_word_completion(cx: &mut TestAppContext) {
 #[gpui::test]
 async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
     init_test(cx, |language_settings| {
-        language_settings.defaults.completions = Some(CompletionSettings {
-            words: WordsCompletionMode::Enabled,
-            words_min_length: 0,
-            lsp: true,
-            lsp_fetch_timeout_ms: 0,
-            lsp_insert_mode: LspInsertMode::Insert,
+        language_settings.defaults.completions = Some(CompletionSettingsContent {
+            words: Some(WordsCompletionMode::Enabled),
+            words_min_length: Some(0),
+            lsp_insert_mode: Some(LspInsertMode::Insert),
+            ..Default::default()
         });
     });
 
@@ -14346,12 +14339,11 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext
 #[gpui::test]
 async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
     init_test(cx, |language_settings| {
-        language_settings.defaults.completions = Some(CompletionSettings {
-            words: WordsCompletionMode::Disabled,
-            words_min_length: 0,
-            lsp: true,
-            lsp_fetch_timeout_ms: 0,
-            lsp_insert_mode: LspInsertMode::Insert,
+        language_settings.defaults.completions = Some(CompletionSettingsContent {
+            words: Some(WordsCompletionMode::Disabled),
+            words_min_length: Some(0),
+            lsp_insert_mode: Some(LspInsertMode::Insert),
+            ..Default::default()
         });
     });
 
@@ -14420,12 +14412,11 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
 #[gpui::test]
 async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
     init_test(cx, |language_settings| {
-        language_settings.defaults.completions = Some(CompletionSettings {
-            words: WordsCompletionMode::Fallback,
-            words_min_length: 0,
-            lsp: false,
-            lsp_fetch_timeout_ms: 0,
-            lsp_insert_mode: LspInsertMode::Insert,
+        language_settings.defaults.completions = Some(CompletionSettingsContent {
+            words_min_length: Some(0),
+            lsp: Some(false),
+            lsp_insert_mode: Some(LspInsertMode::Insert),
+            ..Default::default()
         });
     });
 
@@ -14483,12 +14474,11 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
 #[gpui::test]
 async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
     init_test(cx, |language_settings| {
-        language_settings.defaults.completions = Some(CompletionSettings {
-            words: WordsCompletionMode::Enabled,
-            words_min_length: 3,
-            lsp: true,
-            lsp_fetch_timeout_ms: 0,
-            lsp_insert_mode: LspInsertMode::Insert,
+        language_settings.defaults.completions = Some(CompletionSettingsContent {
+            words: Some(WordsCompletionMode::Enabled),
+            words_min_length: Some(3),
+            lsp_insert_mode: Some(LspInsertMode::Insert),
+            ..Default::default()
         });
     });
 
@@ -14553,12 +14543,11 @@ async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppCont
 #[gpui::test]
 async fn test_word_completions_disabled(cx: &mut TestAppContext) {
     init_test(cx, |language_settings| {
-        language_settings.defaults.completions = Some(CompletionSettings {
-            words: WordsCompletionMode::Enabled,
-            words_min_length: 0,
-            lsp: true,
-            lsp_fetch_timeout_ms: 0,
-            lsp_insert_mode: LspInsertMode::Insert,
+        language_settings.defaults.completions = Some(CompletionSettingsContent {
+            words: Some(WordsCompletionMode::Enabled),
+            words_min_length: Some(0),
+            lsp_insert_mode: Some(LspInsertMode::Insert),
+            ..Default::default()
         });
     });
 
@@ -17067,7 +17056,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
     let _fake_server = fake_servers.next().await.unwrap();
     update_test_language_settings(cx, |language_settings| {
         language_settings.languages.0.insert(
-            language_name.clone(),
+            language_name.clone().0,
             LanguageSettingsContent {
                 tab_size: NonZeroU32::new(8),
                 ..Default::default()
@@ -17991,10 +17980,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
         Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
     )));
     update_test_language_settings(cx, |settings| {
-        settings.defaults.prettier = Some(PrettierSettings {
-            allowed: true,
-            ..PrettierSettings::default()
-        });
+        settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
     });
 
     let test_plugin = "test_plugin";
@@ -19982,7 +19968,8 @@ fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -
             enabled: true,
             line_width: 1,
             active_line_width: 1,
-            ..Default::default()
+            coloring: IndentGuideColoring::default(),
+            background_coloring: IndentGuideBackgroundColoring::default(),
         },
     }
 }
@@ -23672,8 +23659,8 @@ println!("5");
     });
 
     cx.update_global(|store: &mut SettingsStore, cx| {
-        store.update_user_settings::<WorkspaceSettings>(cx, |s| {
-            s.restore_on_file_reopen = Some(false);
+        store.update_user_settings(cx, |s| {
+            s.workspace.restore_on_file_reopen = Some(false);
         });
     });
     editor.update_in(cx, |editor, window, cx| {
@@ -23697,8 +23684,8 @@ println!("5");
         assert!(pane.active_item().is_none());
     });
     cx.update_global(|store: &mut SettingsStore, cx| {
-        store.update_user_settings::<WorkspaceSettings>(cx, |s| {
-            s.restore_on_file_reopen = Some(true);
+        store.update_user_settings(cx, |s| {
+            s.workspace.restore_on_file_reopen = Some(true);
         });
     });
 
@@ -25120,18 +25107,18 @@ pub(crate) fn update_test_language_settings(
 ) {
     cx.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, f);
+            store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
         });
     });
 }
 
 pub(crate) fn update_test_project_settings(
     cx: &mut TestAppContext,
-    f: impl Fn(&mut ProjectSettings),
+    f: impl Fn(&mut ProjectSettingsContent),
 ) {
     cx.update(|cx| {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<ProjectSettings>(cx, f);
+            store.update_user_settings(cx, |settings| f(&mut settings.project));
         });
     });
 }

crates/editor/src/element.rs πŸ”—

@@ -51,9 +51,7 @@ use gpui::{
     transparent_black,
 };
 use itertools::Itertools;
-use language::language_settings::{
-    IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting,
-};
+use language::{IndentGuideSettings, language_settings::ShowWhitespaceSetting};
 use markdown::Markdown;
 use multi_buffer::{
     Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint,
@@ -63,9 +61,12 @@ use multi_buffer::{
 use project::{
     Entry, ProjectPath,
     debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
-    project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
+    project_settings::ProjectSettings,
+};
+use settings::{
+    GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring,
+    Settings,
 };
-use settings::Settings;
 use smallvec::{SmallVec, smallvec};
 use std::{
     any::TypeId,
@@ -2099,10 +2100,7 @@ impl EditorElement {
             .display_diff_hunks_for_rows(display_rows, folded_buffers)
             .map(|hunk| (hunk, None))
             .collect::<Vec<_>>();
-        let git_gutter_setting = ProjectSettings::get_global(cx)
-            .git
-            .git_gutter
-            .unwrap_or_default();
+        let git_gutter_setting = ProjectSettings::get_global(cx).git.git_gutter;
         if let GitGutterSetting::TrackedFiles = git_gutter_setting {
             for (hunk, hitbox) in &mut display_hunks {
                 if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
@@ -2454,11 +2452,7 @@ impl EditorElement {
         let padding = {
             const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.;
 
-            let mut padding = ProjectSettings::get_global(cx)
-                .git
-                .inline_blame
-                .unwrap_or_default()
-                .padding as f32;
+            let mut padding = ProjectSettings::get_global(cx).git.inline_blame.padding as f32;
 
             if let Some(edit_prediction) = editor.active_edit_prediction.as_ref()
                 && let EditPrediction::Edit {
@@ -2492,12 +2486,10 @@ impl EditorElement {
 
             let padded_line_end = line_end + padding;
 
-            let min_column_in_pixels = ProjectSettings::get_global(cx)
-                .git
-                .inline_blame
-                .map(|settings| settings.min_column)
-                .map(|col| self.column_pixels(col as usize, window))
-                .unwrap_or(px(0.));
+            let min_column_in_pixels = self.column_pixels(
+                ProjectSettings::get_global(cx).git.inline_blame.min_column as usize,
+                window,
+            );
             let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
 
             cmp::max(padded_line_end, min_start)
@@ -5673,7 +5665,7 @@ impl EditorElement {
 
         for indent_guide in indent_guides {
             let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
-            let settings = indent_guide.settings;
+            let settings = &indent_guide.settings;
 
             // TODO fixed for now, expose them through themes later
             const INDENT_AWARE_ALPHA: f32 = 0.2;
@@ -6008,7 +6000,7 @@ impl EditorElement {
             .unwrap_or_else(|| {
                 matches!(
                     ProjectSettings::get_global(cx).git.git_gutter,
-                    Some(GitGutterSetting::TrackedFiles)
+                    GitGutterSetting::TrackedFiles
                 )
             });
         if show_git_gutter {
@@ -7303,10 +7295,10 @@ impl EditorElement {
 
     fn diff_hunk_hollow(status: DiffHunkStatus, cx: &mut App) -> bool {
         let unstaged = status.has_secondary_hunk();
-        let unstaged_hollow = ProjectSettings::get_global(cx)
-            .git
-            .hunk_style
-            .is_some_and(|style| matches!(style, GitHunkStyleSetting::UnstagedHollow));
+        let unstaged_hollow = matches!(
+            ProjectSettings::get_global(cx).git.hunk_style,
+            GitHunkStyleSetting::UnstagedHollow
+        );
 
         unstaged == unstaged_hollow
     }
@@ -8843,13 +8835,9 @@ impl Element for EditorElement {
                                 })
                                 .flatten()?;
                             let mut element = render_inline_blame_entry(blame_entry, style, cx)?;
-                            let inline_blame_padding = ProjectSettings::get_global(cx)
-                                .git
-                                .inline_blame
-                                .unwrap_or_default()
-                                .padding
-                                as f32
-                                * em_advance;
+                            let inline_blame_padding =
+                                ProjectSettings::get_global(cx).git.inline_blame.padding as f32
+                                    * em_advance;
                             Some(
                                 element
                                     .layout_as_root(AvailableSpace::min_size(), window, cx)

crates/editor/src/hover_links.rs πŸ”—

@@ -931,8 +931,8 @@ mod tests {
     use futures::StreamExt;
     use gpui::Modifiers;
     use indoc::indoc;
-    use language::language_settings::InlayHintSettings;
     use lsp::request::{GotoDefinition, GotoTypeDefinition};
+    use settings::InlayHintSettingsContent;
     use util::{assert_set_eq, path};
     use workspace::item::Item;
 
@@ -1280,15 +1280,15 @@ mod tests {
     #[gpui::test]
     async fn test_inlay_hover_links(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                enabled: true,
-                show_value_hints: false,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                enabled: Some(true),
+                show_value_hints: Some(false),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });

crates/editor/src/hover_popover.rs πŸ”—

@@ -1004,8 +1004,8 @@ mod tests {
     use collections::BTreeSet;
     use gpui::App;
     use indoc::indoc;
-    use language::language_settings::InlayHintSettings;
     use markdown::parser::MarkdownEvent;
+    use settings::InlayHintSettingsContent;
     use smol::stream::StreamExt;
     use std::sync::atomic;
     use std::sync::atomic::AtomicUsize;
@@ -1551,15 +1551,15 @@ mod tests {
     #[gpui::test]
     async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });

crates/editor/src/inlay_hint_cache.rs πŸ”—

@@ -20,12 +20,14 @@ use anyhow::Context as _;
 use clock::Global;
 use futures::future;
 use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window};
-use language::{Buffer, BufferSnapshot, language_settings::InlayHintKind};
+use language::{
+    Buffer, BufferSnapshot,
+    language_settings::{InlayHintKind, InlayHintSettings},
+};
 use parking_lot::RwLock;
 use project::{InlayHint, ResolveState};
 
 use collections::{HashMap, HashSet, hash_map};
-use language::language_settings::InlayHintSettings;
 use smol::lock::Semaphore;
 use sum_tree::Bias;
 use text::{BufferId, ToOffset, ToPoint};
@@ -1301,13 +1303,13 @@ pub mod tests {
     use futures::StreamExt;
     use gpui::{AppContext as _, Context, SemanticVersion, TestAppContext, WindowHandle};
     use itertools::Itertools as _;
-    use language::{Capability, FakeLspAdapter, language_settings::AllLanguageSettingsContent};
+    use language::{Capability, FakeLspAdapter};
     use language::{Language, LanguageConfig, LanguageMatcher};
     use lsp::FakeLanguageServer;
     use parking_lot::Mutex;
     use project::{FakeFs, Project};
     use serde_json::json;
-    use settings::SettingsStore;
+    use settings::{AllLanguageSettingsContent, InlayHintSettingsContent, SettingsStore};
     use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
     use text::Point;
     use util::path;
@@ -1318,15 +1320,17 @@ pub mod tests {
     async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
         let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
-                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
-                show_other_hints: allowed_hint_kinds.contains(&None),
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(allowed_hint_kinds.contains(&Some(InlayHintKind::Type))),
+                show_parameter_hints: Some(
+                    allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                ),
+                show_other_hints: Some(allowed_hint_kinds.contains(&None)),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -1428,15 +1432,15 @@ pub mod tests {
     #[gpui::test]
     async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -1535,15 +1539,15 @@ pub mod tests {
     #[gpui::test]
     async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -1765,15 +1769,17 @@ pub mod tests {
     async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
         let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
-                show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
-                show_other_hints: allowed_hint_kinds.contains(&None),
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(allowed_hint_kinds.contains(&Some(InlayHintKind::Type))),
+                show_parameter_hints: Some(
+                    allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                ),
+                show_other_hints: Some(allowed_hint_kinds.contains(&None)),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -1926,16 +1932,19 @@ pub mod tests {
             ),
         ] {
             update_test_language_settings(cx, |settings| {
-                settings.defaults.inlay_hints = Some(InlayHintSettings {
-                    show_value_hints: true,
-                    enabled: true,
-                    edit_debounce_ms: 0,
-                    scroll_debounce_ms: 0,
-                    show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
-                    show_parameter_hints: new_allowed_hint_kinds
-                        .contains(&Some(InlayHintKind::Parameter)),
-                    show_other_hints: new_allowed_hint_kinds.contains(&None),
-                    show_background: false,
+                settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                    show_value_hints: Some(true),
+                    enabled: Some(true),
+                    edit_debounce_ms: Some(0),
+                    scroll_debounce_ms: Some(0),
+                    show_type_hints: Some(
+                        new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                    ),
+                    show_parameter_hints: Some(
+                        new_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                    ),
+                    show_other_hints: Some(new_allowed_hint_kinds.contains(&None)),
+                    show_background: Some(false),
                     toggle_on_modifiers_press: None,
                 })
             });
@@ -1970,16 +1979,19 @@ pub mod tests {
 
         let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
         update_test_language_settings(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: false,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
-                show_parameter_hints: another_allowed_hint_kinds
-                    .contains(&Some(InlayHintKind::Parameter)),
-                show_other_hints: another_allowed_hint_kinds.contains(&None),
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(false),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(
+                    another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                ),
+                show_parameter_hints: Some(
+                    another_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                ),
+                show_other_hints: Some(another_allowed_hint_kinds.contains(&None)),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -2027,16 +2039,19 @@ pub mod tests {
 
         let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
         update_test_language_settings(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
-                show_parameter_hints: final_allowed_hint_kinds
-                    .contains(&Some(InlayHintKind::Parameter)),
-                show_other_hints: final_allowed_hint_kinds.contains(&None),
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(
+                    final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+                ),
+                show_parameter_hints: Some(
+                    final_allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+                ),
+                show_other_hints: Some(final_allowed_hint_kinds.contains(&None)),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -2102,15 +2117,15 @@ pub mod tests {
     #[gpui::test]
     async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -2239,15 +2254,15 @@ pub mod tests {
     #[gpui::test(iterations = 10)]
     async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -2540,15 +2555,15 @@ pub mod tests {
     #[gpui::test]
     async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -2864,15 +2879,15 @@ pub mod tests {
     #[gpui::test]
     async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: false,
-                show_parameter_hints: false,
-                show_other_hints: false,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(false),
+                show_parameter_hints: Some(false),
+                show_other_hints: Some(false),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -3041,15 +3056,15 @@ pub mod tests {
             .unwrap();
 
         update_test_language_settings(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -3074,15 +3089,15 @@ pub mod tests {
     #[gpui::test]
     async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -3167,15 +3182,15 @@ pub mod tests {
     #[gpui::test]
     async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: false,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(false),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -3244,15 +3259,15 @@ pub mod tests {
             .unwrap();
 
         update_test_language_settings(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });
@@ -3305,15 +3320,15 @@ pub mod tests {
     #[gpui::test]
     async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.inlay_hints = Some(InlayHintSettings {
-                show_value_hints: true,
-                enabled: true,
-                edit_debounce_ms: 0,
-                scroll_debounce_ms: 0,
-                show_type_hints: true,
-                show_parameter_hints: true,
-                show_other_hints: true,
-                show_background: false,
+            settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
+                show_value_hints: Some(true),
+                enabled: Some(true),
+                edit_debounce_ms: Some(0),
+                scroll_debounce_ms: Some(0),
+                show_type_hints: Some(true),
+                show_parameter_hints: Some(true),
+                show_other_hints: Some(true),
+                show_background: Some(false),
                 toggle_on_modifiers_press: None,
             })
         });

crates/editor/src/jsx_tag_auto_close.rs πŸ”—

@@ -328,7 +328,7 @@ pub(crate) fn refresh_enabled_in_any_buffer(
                     snapshot.file(),
                     cx,
                 );
-                if language_settings.jsx_tag_auto_close.enabled {
+                if language_settings.jsx_tag_auto_close {
                     found_enabled = true;
                 }
             }
@@ -406,7 +406,7 @@ pub(crate) fn handle_from(
             };
 
             let language_settings = snapshot.settings_at(edit.new.end, cx);
-            if !language_settings.jsx_tag_auto_close.enabled {
+            if !language_settings.jsx_tag_auto_close {
                 continue;
             }
 
@@ -620,14 +620,17 @@ mod jsx_tag_autoclose_tests {
 
     use super::*;
     use gpui::{AppContext as _, TestAppContext};
-    use language::language_settings::JsxTagAutoCloseSettings;
     use languages::language;
     use multi_buffer::ExcerptRange;
     use text::Selection;
 
     async fn test_setup(cx: &mut TestAppContext) -> EditorTestContext {
         init_test(cx, |settings| {
-            settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
+            settings
+                .defaults
+                .jsx_tag_auto_close
+                .get_or_insert_default()
+                .enabled = Some(true);
         });
 
         let mut cx = EditorTestContext::new(cx).await;
@@ -789,7 +792,11 @@ mod jsx_tag_autoclose_tests {
     #[gpui::test]
     async fn test_multibuffer(cx: &mut TestAppContext) {
         init_test(cx, |settings| {
-            settings.defaults.jsx_tag_auto_close = Some(JsxTagAutoCloseSettings { enabled: true });
+            settings
+                .defaults
+                .jsx_tag_auto_close
+                .get_or_insert_default()
+                .enabled = Some(true);
         });
 
         let buffer_a = cx.new(|cx| {

crates/eval/src/eval.rs πŸ”—

@@ -340,10 +340,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
     release_channel::init(app_version, cx);
     gpui_tokio::init(cx);
 
-    let mut settings_store = SettingsStore::new(cx);
-    settings_store
-        .set_default_settings(settings::default_settings().as_ref(), cx)
-        .unwrap();
+    let settings_store = SettingsStore::new(cx, &settings::default_settings());
     cx.set_global(settings_store);
     client::init_settings(cx);
 

crates/extension_host/Cargo.toml πŸ”—

@@ -37,7 +37,6 @@ paths.workspace = true
 project.workspace = true
 remote.workspace = true
 release_channel.workspace = true
-schemars.workspace = true
 semantic_version.workspace = true
 serde.workspace = true
 serde_json.workspace = true

crates/extension_host/src/extension_settings.rs πŸ”—

@@ -1,13 +1,9 @@
-use anyhow::Result;
 use collections::HashMap;
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::Settings;
 use std::sync::Arc;
 
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
+#[derive(Debug, Default, Clone)]
 pub struct ExtensionSettings {
     /// The extensions that should be automatically installed by Zed.
     ///
@@ -15,9 +11,7 @@ pub struct ExtensionSettings {
     /// available out-of-the-box.
     ///
     /// Default: { "html": true }
-    #[serde(default)]
     pub auto_install_extensions: HashMap<Arc<str>, bool>,
-    #[serde(default)]
     pub auto_update_extensions: HashMap<Arc<str>, bool>,
 }
 
@@ -39,18 +33,24 @@ impl ExtensionSettings {
 }
 
 impl Settings for ExtensionSettings {
-    type FileContent = Self;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        Self {
+            auto_install_extensions: content.extension.auto_install_extensions.clone(),
+            auto_update_extensions: content.extension.auto_update_extensions.clone(),
+        }
+    }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
-        SettingsSources::<Self::FileContent>::json_merge_with(
-            [sources.default]
-                .into_iter()
-                .chain(sources.user)
-                .chain(sources.server),
-        )
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        self.auto_install_extensions
+            .extend(content.extension.auto_install_extensions.clone());
+        self.auto_update_extensions
+            .extend(content.extension.auto_update_extensions.clone());
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {
+    fn import_from_vscode(
+        _vscode: &settings::VsCodeSettings,
+        _current: &mut settings::SettingsContent,
+    ) {
         // settingsSync.ignoredExtensions controls autoupdate for vscode extensions, but we
         // don't have a mapping to zed-extensions. there's also extensions.autoCheckUpdates
         // and extensions.autoUpdate which are global switches, we don't support those yet

crates/extensions_ui/src/extension_version_selector.rs πŸ”—

@@ -2,7 +2,7 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use client::ExtensionMetadata;
-use extension_host::{ExtensionSettings, ExtensionStore};
+use extension_host::ExtensionStore;
 use fs::Fs;
 use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
 use gpui::{App, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, prelude::*};
@@ -183,10 +183,13 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
             let extension_id = extension_version.id.clone();
             let version = extension_version.manifest.version.clone();
 
-            update_settings_file::<ExtensionSettings>(self.fs.clone(), cx, {
+            update_settings_file(self.fs.clone(), cx, {
                 let extension_id = extension_id.clone();
                 move |settings, _| {
-                    settings.auto_update_extensions.insert(extension_id, false);
+                    settings
+                        .extension
+                        .auto_update_extensions
+                        .insert(extension_id, false);
                 }
             });
 

crates/extensions_ui/src/extensions_ui.rs πŸ”—

@@ -20,7 +20,7 @@ use gpui::{
 use num_format::{Locale, ToFormattedString};
 use project::DirectoryLister;
 use release_channel::ReleaseChannel;
-use settings::Settings;
+use settings::{Settings, SettingsContent};
 use strum::IntoEnumIterator as _;
 use theme::ThemeSettings;
 use ui::{
@@ -1269,17 +1269,17 @@ impl ExtensionsPage {
         Label::new(message)
     }
 
-    fn update_settings<T: Settings>(
+    fn update_settings(
         &mut self,
         selection: &ToggleState,
 
         cx: &mut Context<Self>,
-        callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
+        callback: impl 'static + Send + Fn(&mut SettingsContent, bool),
     ) {
         if let Some(workspace) = self.workspace.upgrade() {
             let fs = workspace.read(cx).app_state().fs.clone();
             let selection = *selection;
-            settings::update_settings_file::<T>(fs, cx, move |settings, _| {
+            settings::update_settings_file(fs, cx, move |settings, _| {
                 let value = match selection {
                     ToggleState::Unselected => false,
                     ToggleState::Selected => true,
@@ -1340,11 +1340,9 @@ impl ExtensionsPage {
                         },
                         cx.listener(move |this, selection, _, cx| {
                             telemetry::event!("Vim Mode Toggled", source = "Feature Upsell");
-                            this.update_settings::<VimModeSetting>(
-                                selection,
-                                cx,
-                                |setting, value| setting.vim_mode = Some(value),
-                            );
+                            this.update_settings(selection, cx, |setting, value| {
+                                setting.vim_mode = Some(value)
+                            });
                         }),
                     )),
                 Feature::LanguageBash => FeatureUpsell::new("Shell support is built-in to Zed!")

crates/file_finder/src/file_finder.rs πŸ”—

@@ -347,16 +347,16 @@ impl FileFinder {
         })
     }
 
-    pub fn modal_max_width(width_setting: Option<FileFinderWidth>, window: &mut Window) -> Pixels {
+    pub fn modal_max_width(width_setting: FileFinderWidth, window: &mut Window) -> Pixels {
         let window_width = window.viewport_size().width;
         let small_width = rems(34.).to_pixels(window.rem_size());
 
         match width_setting {
-            None | Some(FileFinderWidth::Small) => small_width,
-            Some(FileFinderWidth::Full) => window_width,
-            Some(FileFinderWidth::XLarge) => (window_width - Pixels(512.)).max(small_width),
-            Some(FileFinderWidth::Large) => (window_width - Pixels(768.)).max(small_width),
-            Some(FileFinderWidth::Medium) => (window_width - Pixels(1024.)).max(small_width),
+            FileFinderWidth::Small => small_width,
+            FileFinderWidth::Full => window_width,
+            FileFinderWidth::XLarge => (window_width - Pixels(512.)).max(small_width),
+            FileFinderWidth::Large => (window_width - Pixels(768.)).max(small_width),
+            FileFinderWidth::Medium => (window_width - Pixels(1024.)).max(small_width),
         }
     }
 }

crates/file_finder/src/file_finder_settings.rs πŸ”—

@@ -1,55 +1,41 @@
-use anyhow::Result;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::Settings;
+use util::MergeFrom;
 
 #[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
 pub struct FileFinderSettings {
     pub file_icons: bool,
-    pub modal_max_width: Option<FileFinderWidth>,
+    pub modal_max_width: FileFinderWidth,
     pub skip_focus_for_active_in_search: bool,
     pub include_ignored: Option<bool>,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "file_finder")]
-pub struct FileFinderSettingsContent {
-    /// Whether to show file icons in the file finder.
-    ///
-    /// Default: true
-    pub file_icons: Option<bool>,
-    /// Determines how much space the file finder can take up in relation to the available window width.
-    ///
-    /// Default: small
-    pub modal_max_width: Option<FileFinderWidth>,
-    /// Determines whether the file finder should skip focus for the active file in search results.
-    ///
-    /// Default: true
-    pub skip_focus_for_active_in_search: Option<bool>,
-    /// Determines whether to show the git status in the file finder
-    ///
-    /// Default: true
-    pub git_status: Option<bool>,
-    /// Whether to use gitignored files when searching.
-    /// Only the file Zed had indexed will be used, not necessary all the gitignored files.
-    ///
-    /// Can accept 3 values:
-    /// * `Some(true)`: Use all gitignored files
-    /// * `Some(false)`: Use only the files Zed had indexed
-    /// * `None`: Be smart and search for ignored when called from a gitignored worktree
-    ///
-    /// Default: None
-    pub include_ignored: Option<Option<bool>>,
-}
-
 impl Settings for FileFinderSettings {
-    type FileContent = FileFinderSettingsContent;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+        let file_finder = content.file_finder.as_ref().unwrap();
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut gpui::App) -> Result<Self> {
-        sources.json_merge()
+        Self {
+            file_icons: file_finder.file_icons.unwrap(),
+            modal_max_width: file_finder.modal_max_width.unwrap().into(),
+            skip_focus_for_active_in_search: file_finder.skip_focus_for_active_in_search.unwrap(),
+            include_ignored: file_finder.include_ignored.flatten(),
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) {
+        let Some(file_finder) = content.file_finder.as_ref() else {
+            return;
+        };
+
+        self.file_icons.merge_from(&file_finder.file_icons);
+        self.modal_max_width
+            .merge_from(&file_finder.modal_max_width.map(Into::into));
+        self.skip_focus_for_active_in_search
+            .merge_from(&file_finder.skip_focus_for_active_in_search);
+        self.include_ignored
+            .merge_from(&file_finder.include_ignored);
+    }
 }
 
 #[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
@@ -62,3 +48,15 @@ pub enum FileFinderWidth {
     XLarge,
     Full,
 }
+
+impl From<settings::FileFinderWidthContent> for FileFinderWidth {
+    fn from(content: settings::FileFinderWidthContent) -> Self {
+        match content {
+            settings::FileFinderWidthContent::Small => FileFinderWidth::Small,
+            settings::FileFinderWidthContent::Medium => FileFinderWidth::Medium,
+            settings::FileFinderWidthContent::Large => FileFinderWidth::Large,
+            settings::FileFinderWidthContent::XLarge => FileFinderWidth::XLarge,
+            settings::FileFinderWidthContent::Full => FileFinderWidth::Full,
+        }
+    }
+}

crates/git_hosting_providers/Cargo.toml πŸ”—

@@ -19,7 +19,6 @@ git.workspace = true
 gpui.workspace = true
 http_client.workspace = true
 regex.workspace = true
-schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/git_hosting_providers/src/settings.rs πŸ”—

@@ -1,11 +1,8 @@
 use std::sync::Arc;
 
-use anyhow::Result;
 use git::GitHostingProviderRegistry;
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsStore, SettingsUi};
+use settings::{GitHostingProviderConfig, GitHostingProviderKind, Settings, SettingsStore};
 use url::Url;
 use util::ResultExt as _;
 
@@ -55,43 +52,23 @@ fn update_git_hosting_providers_from_settings(cx: &mut App) {
     provider_registry.set_setting_providers(iter);
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitHostingProviderKind {
-    Github,
-    Gitlab,
-    Bitbucket,
-}
-
-/// A custom Git hosting provider.
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
-pub struct GitHostingProviderConfig {
-    /// The type of the provider.
-    ///
-    /// Must be one of `github`, `gitlab`, or `bitbucket`.
-    pub provider: GitHostingProviderKind,
-
-    /// The base URL for the provider (e.g., "https://code.corp.big.com").
-    pub base_url: String,
-
-    /// The display name for the provider (e.g., "BigCorp GitHub").
-    pub name: String,
-}
-
-#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
+#[derive(Debug, Clone)]
 pub struct GitHostingProviderSettings {
-    /// The list of custom Git hosting providers.
-    #[serde(default)]
     pub git_hosting_providers: Vec<GitHostingProviderConfig>,
 }
 
 impl Settings for GitHostingProviderSettings {
-    type FileContent = Self;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        Self {
+            git_hosting_providers: content.project.git_hosting_providers.clone().unwrap(),
+        }
+    }
 
-    fn load(sources: settings::SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
+        if let Some(more) = &content.project.git_hosting_providers {
+            self.git_hosting_providers.extend_from_slice(&more.clone());
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut settings::SettingsContent) {}
 }

crates/git_ui/src/blame_ui.rs πŸ”—

@@ -139,7 +139,8 @@ impl BlameRenderer for GitBlameRenderer {
         let author = blame_entry.author.as_deref().unwrap_or_default();
         let summary_enabled = ProjectSettings::get_global(cx)
             .git
-            .show_inline_commit_summary();
+            .inline_blame
+            .show_commit_summary;
 
         let text = match blame_entry.summary.as_ref() {
             Some(summary) if summary_enabled => {

crates/git_ui/src/branch_picker.rs πŸ”—

@@ -564,7 +564,6 @@ impl PickerDelegate for BranchListDelegate {
                                     let show_author_name = ProjectSettings::get_global(cx)
                                         .git
                                         .branch_picker
-                                        .unwrap_or_default()
                                         .show_author_name;
 
                                     subject.map_or("no commits found".into(), |subject| {

crates/git_ui/src/git_panel.rs πŸ”—

@@ -2,7 +2,6 @@ use crate::askpass_modal::AskPassModal;
 use crate::commit_modal::CommitModal;
 use crate::commit_tooltip::CommitTooltip;
 use crate::commit_view::CommitView;
-use crate::git_panel_settings::StatusStyle;
 use crate::project_diff::{self, Diff, ProjectDiff};
 use crate::remote_output::{self, RemoteAction, SuccessMessage};
 use crate::{branch_picker, picker_prompt, render_remote_button};
@@ -51,7 +50,7 @@ use project::{
     git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId},
 };
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
+use settings::{Settings, SettingsStore, StatusStyle};
 use std::future::Future;
 use std::ops::Range;
 use std::path::{Path, PathBuf};
@@ -2439,8 +2438,9 @@ impl GitPanel {
             let workspace = workspace.read(cx);
             let fs = workspace.app_state().fs.clone();
             cx.update_global::<SettingsStore, _>(|store, _cx| {
-                store.update_settings_file::<GitPanelSettings>(fs, move |settings, _cx| {
-                    settings.sort_by_path = Some(!current_setting);
+                store.update_settings_file(fs, move |settings, _cx| {
+                    settings.git_panel.get_or_insert_default().sort_by_path =
+                        Some(!current_setting);
                 });
             });
         }
@@ -4356,11 +4356,9 @@ impl Panel for GitPanel {
     }
 
     fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
-        settings::update_settings_file::<GitPanelSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings, _| settings.dock = Some(position),
-        );
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            settings.git_panel.get_or_insert_default().dock = Some(position.into())
+        });
     }
 
     fn size(&self, _: &Window, cx: &App) -> Pixels {

crates/git_ui/src/git_panel_settings.rs πŸ”—

@@ -2,8 +2,12 @@ use editor::EditorSettings;
 use gpui::Pixels;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
-use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
+use settings::{Settings, SettingsContent, StatusStyle};
+use ui::{
+    px,
+    scrollbars::{ScrollbarVisibility, ShowScrollbar},
+};
+use util::MergeFrom;
 use workspace::dock::DockPosition;
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -19,67 +23,7 @@ pub struct ScrollbarSettings {
     pub show: Option<ShowScrollbar>,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-// Style of the git status indicator in the panel.
-//
-// Default: icon
-pub enum StatusStyleContent {
-    Icon,
-    LabelColor,
-}
-
-#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum StatusStyle {
-    #[default]
-    Icon,
-    LabelColor,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "git_panel")]
-pub struct GitPanelSettingsContent {
-    /// Whether to show the panel button in the status bar.
-    ///
-    /// Default: true
-    pub button: Option<bool>,
-    /// Where to dock the panel.
-    ///
-    /// Default: left
-    pub dock: Option<DockPosition>,
-    /// Default width of the panel in pixels.
-    ///
-    /// Default: 360
-    pub default_width: Option<f32>,
-    /// How entry statuses are displayed.
-    ///
-    /// Default: icon
-    pub status_style: Option<StatusStyle>,
-    /// How and when the scrollbar should be displayed.
-    ///
-    /// Default: inherits editor scrollbar settings
-    pub scrollbar: Option<ScrollbarSettings>,
-
-    /// What the default branch name should be when
-    /// `init.defaultBranch` is not set in git
-    ///
-    /// Default: main
-    pub fallback_branch_name: Option<String>,
-
-    /// Whether to sort entries in the panel by path
-    /// or by status (the default).
-    ///
-    /// Default: false
-    pub sort_by_path: Option<bool>,
-
-    /// Whether to collapse untracked files in the diff panel.
-    ///
-    /// Default: false
-    pub collapse_untracked_diff: Option<bool>,
-}
-
-#[derive(Deserialize, Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct GitPanelSettings {
     pub button: bool,
     pub dock: DockPosition,
@@ -108,17 +52,50 @@ impl ScrollbarVisibility for GitPanelSettings {
 }
 
 impl Settings for GitPanelSettings {
-    type FileContent = GitPanelSettingsContent;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+        let git_panel = content.git_panel.clone().unwrap();
+        Self {
+            button: git_panel.button.unwrap(),
+            dock: git_panel.dock.unwrap().into(),
+            default_width: px(git_panel.default_width.unwrap()),
+            status_style: git_panel.status_style.unwrap(),
+            scrollbar: ScrollbarSettings {
+                show: git_panel.scrollbar.unwrap().show.map(Into::into),
+            },
+            fallback_branch_name: git_panel.fallback_branch_name.unwrap(),
+            sort_by_path: git_panel.sort_by_path.unwrap(),
+            collapse_untracked_diff: git_panel.collapse_untracked_diff.unwrap(),
+        }
+    }
 
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _: &mut gpui::App,
-    ) -> anyhow::Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut ui::App) {
+        let Some(git_panel) = &content.git_panel else {
+            return;
+        };
+        self.button.merge_from(&git_panel.button);
+        self.dock.merge_from(&git_panel.dock.map(Into::into));
+        self.default_width
+            .merge_from(&git_panel.default_width.map(px));
+        self.status_style.merge_from(&git_panel.status_style);
+        self.fallback_branch_name
+            .merge_from(&git_panel.fallback_branch_name);
+        self.sort_by_path.merge_from(&git_panel.sort_by_path);
+        self.collapse_untracked_diff
+            .merge_from(&git_panel.collapse_untracked_diff);
+        if let Some(show) = git_panel.scrollbar.as_ref().and_then(|s| s.show) {
+            self.scrollbar.show = Some(show.into())
+        }
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
-        vscode.bool_setting("git.enabled", &mut current.button);
-        vscode.string_setting("git.defaultBranchName", &mut current.fallback_branch_name);
+    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
+        if let Some(git_enabled) = vscode.read_bool("git.enabled") {
+            current.git_panel.get_or_insert_default().button = Some(git_enabled);
+        }
+        if let Some(default_branch) = vscode.read_string("git.defaultBranchName") {
+            current
+                .git_panel
+                .get_or_insert_default()
+                .fallback_branch_name = Some(default_branch.to_string());
+        }
     }
 }

crates/go_to_line/Cargo.toml πŸ”—

@@ -13,12 +13,10 @@ path = "src/go_to_line.rs"
 doctest = false
 
 [dependencies]
-anyhow.workspace = true
 editor.workspace = true
 gpui.workspace = true
 language.workspace = true
 menu.workspace = true
-schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
 text.workspace = true

crates/go_to_line/src/cursor_position.rs πŸ”—

@@ -1,15 +1,13 @@
 use editor::{Editor, EditorSettings, MultiBufferSnapshot};
 use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::Settings;
 use std::{fmt::Write, num::NonZeroU32, time::Duration};
 use text::{Point, Selection};
 use ui::{
     Button, ButtonCommon, Clickable, Context, FluentBuilder, IntoElement, LabelSize, ParentElement,
     Render, Tooltip, Window, div,
 };
-use util::paths::FILE_ROW_COLUMN_DELIMITER;
+use util::{MergeFrom, paths::FILE_ROW_COLUMN_DELIMITER};
 use workspace::{StatusItemView, Workspace, item::ItemHandle};
 
 #[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq)]
@@ -293,41 +291,27 @@ impl StatusItemView for CursorPosition {
     }
 }
 
-#[derive(Clone, Copy, Default, PartialEq, Debug, JsonSchema, Deserialize, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub(crate) enum LineIndicatorFormat {
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum LineIndicatorFormat {
     Short,
-    #[default]
     Long,
 }
 
-#[derive(
-    Clone, Copy, Default, Debug, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey,
-)]
-#[settings_key(None)]
-pub(crate) struct LineIndicatorFormatContent {
-    line_indicator_format: Option<LineIndicatorFormat>,
+impl From<settings::LineIndicatorFormat> for LineIndicatorFormat {
+    fn from(format: settings::LineIndicatorFormat) -> Self {
+        match format {
+            settings::LineIndicatorFormat::Short => LineIndicatorFormat::Short,
+            settings::LineIndicatorFormat::Long => LineIndicatorFormat::Long,
+        }
+    }
 }
 
 impl Settings for LineIndicatorFormat {
-    type FileContent = LineIndicatorFormatContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
-        let format = [
-            sources.release_channel,
-            sources.profile,
-            sources.user,
-            sources.operating_system,
-            Some(sources.default),
-        ]
-        .into_iter()
-        .flatten()
-        .filter_map(|val| val.line_indicator_format)
-        .next()
-        .ok_or_else(Self::missing_default)?;
-
-        Ok(format)
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        content.line_indicator_format.unwrap().into()
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        self.merge_from(&content.line_indicator_format.map(Into::into));
+    }
 }

crates/google_ai/Cargo.toml πŸ”—

@@ -21,5 +21,6 @@ http_client.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
+settings.workspace = true
 strum.workspace = true
 workspace-hack.workspace = true

crates/google_ai/src/google_ai.rs πŸ”—

@@ -4,6 +4,7 @@ use anyhow::{Result, anyhow, bail};
 use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
 use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
+pub use settings::ModelMode as GoogleModelMode;
 
 pub const API_URL: &str = "https://generativelanguage.googleapis.com";
 
@@ -295,16 +296,6 @@ pub struct ThinkingConfig {
     pub thinking_budget: u32,
 }
 
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
-pub enum GoogleModelMode {
-    #[default]
-    Default,
-    Thinking {
-        budget_tokens: Option<u32>,
-    },
-}
-
 #[derive(Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct GenerationConfig {

crates/image_viewer/Cargo.toml πŸ”—

@@ -24,7 +24,6 @@ gpui.workspace = true
 language.workspace = true
 log.workspace = true
 project.workspace = true
-schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
 theme.workspace = true

crates/image_viewer/src/image_viewer_settings.rs πŸ”—

@@ -1,40 +1,30 @@
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+pub use settings::ImageFileSizeUnit;
+use settings::Settings;
+use util::MergeFrom;
 
 /// The settings for the image viewer.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, SettingsUi, SettingsKey)]
-#[settings_key(key = "image_viewer")]
+#[derive(Clone, Debug, Default)]
 pub struct ImageViewerSettings {
     /// The unit to use for displaying image file sizes.
     ///
     /// Default: "binary"
-    #[serde(default)]
     pub unit: ImageFileSizeUnit,
 }
 
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default)]
-#[serde(rename_all = "snake_case")]
-pub enum ImageFileSizeUnit {
-    /// Displays file size in binary units (e.g., KiB, MiB).
-    #[default]
-    Binary,
-    /// Displays file size in decimal units (e.g., KB, MB).
-    Decimal,
-}
-
 impl Settings for ImageViewerSettings {
-    type FileContent = Self;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
-        SettingsSources::<Self::FileContent>::json_merge_with(
-            [sources.default]
-                .into_iter()
-                .chain(sources.user)
-                .chain(sources.server),
-        )
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        Self {
+            unit: content.image_viewer.clone().unwrap().unit.unwrap(),
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        self.unit.merge_from(
+            &content
+                .image_viewer
+                .as_ref()
+                .and_then(|image_viewer| image_viewer.unit),
+        );
+    }
 }

crates/journal/Cargo.toml πŸ”—

@@ -18,12 +18,12 @@ chrono.workspace = true
 editor.workspace = true
 gpui.workspace = true
 log.workspace = true
-schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
 shellexpand.workspace = true
 workspace.workspace = true
 workspace-hack.workspace = true
+util.workspace = true
 
 [dev-dependencies]
 editor = { workspace = true, features = ["test-support"] }

crates/journal/src/journal.rs πŸ”—

@@ -1,16 +1,15 @@
-use anyhow::Result;
 use chrono::{Datelike, Local, NaiveTime, Timelike};
 use editor::scroll::Autoscroll;
 use editor::{Editor, SelectionEffects};
 use gpui::{App, AppContext as _, Context, Window, actions};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+pub use settings::HourFormat;
+use settings::Settings;
 use std::{
     fs::OpenOptions,
     path::{Path, PathBuf},
     sync::Arc,
 };
+use util::MergeFrom;
 use workspace::{AppState, OpenVisible, Workspace};
 
 actions!(
@@ -22,44 +21,35 @@ actions!(
 );
 
 /// Settings specific to journaling
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(key = "journal")]
+#[derive(Clone, Debug)]
 pub struct JournalSettings {
     /// The path of the directory where journal entries are stored.
     ///
     /// Default: `~`
-    pub path: Option<String>,
+    pub path: String,
     /// What format to display the hours in.
     ///
     /// Default: hour12
-    pub hour_format: Option<HourFormat>,
+    pub hour_format: HourFormat,
 }
 
-impl Default for JournalSettings {
-    fn default() -> Self {
+impl settings::Settings for JournalSettings {
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let journal = content.journal.clone().unwrap();
+
         Self {
-            path: Some("~".into()),
-            hour_format: Some(Default::default()),
+            path: journal.path.unwrap(),
+            hour_format: journal.hour_format.unwrap(),
         }
     }
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum HourFormat {
-    #[default]
-    Hour12,
-    Hour24,
-}
 
-impl settings::Settings for JournalSettings {
-    type FileContent = Self;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(journal) = content.journal.as_ref() else {
+            return;
+        };
+        self.path.merge_from(&journal.path);
+        self.hour_format.merge_from(&journal.hour_format);
     }
-
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
 }
 
 pub fn init(_: Arc<AppState>, cx: &mut App) {
@@ -77,7 +67,7 @@ pub fn init(_: Arc<AppState>, cx: &mut App) {
 
 pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut App) {
     let settings = JournalSettings::get_global(cx);
-    let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) {
+    let journal_dir = match journal_dir(&settings.path) {
         Some(journal_dir) => journal_dir,
         None => {
             log::error!("Can't determine journal directory");
@@ -199,13 +189,13 @@ fn journal_dir(path: &str) -> Option<PathBuf> {
         .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"))
 }
 
-fn heading_entry(now: NaiveTime, hour_format: &Option<HourFormat>) -> String {
+fn heading_entry(now: NaiveTime, hour_format: &HourFormat) -> String {
     match hour_format {
-        Some(HourFormat::Hour24) => {
+        HourFormat::Hour24 => {
             let hour = now.hour();
             format!("# {}:{:02}", hour, now.minute())
         }
-        _ => {
+        HourFormat::Hour12 => {
             let (pm, hour) = now.hour12();
             let am_or_pm = if pm { "PM" } else { "AM" };
             format!("# {}:{:02} {}", hour, now.minute(), am_or_pm)
@@ -221,7 +211,7 @@ mod tests {
         #[test]
         fn test_heading_entry_defaults_to_hour_12() {
             let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
-            let actual_heading_entry = heading_entry(naive_time, &None);
+            let actual_heading_entry = heading_entry(naive_time, &HourFormat::Hour12);
             let expected_heading_entry = "# 3:00 PM";
 
             assert_eq!(actual_heading_entry, expected_heading_entry);
@@ -230,7 +220,7 @@ mod tests {
         #[test]
         fn test_heading_entry_is_hour_12() {
             let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
-            let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour12));
+            let actual_heading_entry = heading_entry(naive_time, &HourFormat::Hour12);
             let expected_heading_entry = "# 3:00 PM";
 
             assert_eq!(actual_heading_entry, expected_heading_entry);
@@ -239,7 +229,7 @@ mod tests {
         #[test]
         fn test_heading_entry_is_hour_24() {
             let naive_time = NaiveTime::from_hms_milli_opt(15, 0, 0, 0).unwrap();
-            let actual_heading_entry = heading_entry(naive_time, &Some(HourFormat::Hour24));
+            let actual_heading_entry = heading_entry(naive_time, &HourFormat::Hour24);
             let expected_heading_entry = "# 15:00";
 
             assert_eq!(actual_heading_entry, expected_heading_entry);

crates/language/src/buffer.rs πŸ”—

@@ -30,10 +30,9 @@ use gpui::{
 
 use lsp::{LanguageServerId, NumberOrString};
 use parking_lot::Mutex;
-use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use settings::{SettingsUi, WorktreeId};
+use settings::WorktreeId;
 use smallvec::SmallVec;
 use smol::future::yield_now;
 use std::{
@@ -174,10 +173,7 @@ pub enum IndentKind {
 }
 
 /// The shape of a selection cursor.
-#[derive(
-    Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
 pub enum CursorShape {
     /// A vertical bar
     #[default]
@@ -190,6 +186,17 @@ pub enum CursorShape {
     Hollow,
 }
 
+impl From<settings::CursorShape> for CursorShape {
+    fn from(shape: settings::CursorShape) -> Self {
+        match shape {
+            settings::CursorShape::Bar => CursorShape::Bar,
+            settings::CursorShape::Block => CursorShape::Block,
+            settings::CursorShape::Underline => CursorShape::Underline,
+            settings::CursorShape::Hollow => CursorShape::Hollow,
+        }
+    }
+}
+
 #[derive(Clone, Debug)]
 struct SelectionSet {
     line_mode: bool,

crates/language/src/buffer_tests.rs πŸ”—

@@ -1,8 +1,5 @@
 use super::*;
 use crate::Buffer;
-use crate::language_settings::{
-    AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent,
-};
 use clock::ReplicaId;
 use collections::BTreeMap;
 use futures::FutureExt as _;
@@ -13,6 +10,7 @@ use proto::deserialize_operation;
 use rand::prelude::*;
 use regex::RegexBuilder;
 use settings::SettingsStore;
+use settings::{AllLanguageSettingsContent, LanguageSettingsContent};
 use std::collections::BTreeSet;
 use std::{
     env,
@@ -3849,7 +3847,7 @@ fn init_settings(cx: &mut App, f: fn(&mut AllLanguageSettingsContent)) {
     cx.set_global(settings_store);
     crate::init(cx);
     cx.update_global::<SettingsStore, _>(|settings, cx| {
-        settings.update_user_settings::<AllLanguageSettings>(cx, f);
+        settings.update_user_settings(cx, |content| f(&mut content.project.all_languages));
     });
 }
 

crates/language/src/language.rs πŸ”—

@@ -22,8 +22,8 @@ mod toolchain;
 #[cfg(test)]
 pub mod buffer_tests;
 
-pub use crate::language_settings::EditPredictionsMode;
 use crate::language_settings::SoftWrap;
+pub use crate::language_settings::{EditPredictionsMode, IndentGuideSettings};
 use anyhow::{Context as _, Result};
 use async_trait::async_trait;
 use collections::{HashMap, HashSet, IndexSet};

crates/language/src/language_registry.rs πŸ”—

@@ -1,14 +1,11 @@
 use crate::{
     CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher,
     LanguageServerName, LspAdapter, ManifestName, PLAIN_TEXT, ToolchainLister,
-    language_settings::{
-        AllLanguageSettingsContent, LanguageSettingsContent, all_language_settings,
-    },
-    task_context::ContextProvider,
-    with_parser,
+    language_settings::all_language_settings, task_context::ContextProvider, with_parser,
 };
 use anyhow::{Context as _, Result, anyhow};
 use collections::{FxHashMap, HashMap, HashSet, hash_map};
+use settings::{AllLanguageSettingsContent, LanguageSettingsContent};
 
 use futures::{
     Future,
@@ -1175,7 +1172,7 @@ impl LanguageRegistryState {
             language.set_theme(theme.syntax());
         }
         self.language_settings.languages.0.insert(
-            language.name(),
+            language.name().0,
             LanguageSettingsContent {
                 tab_size: language.config.tab_size,
                 hard_tabs: language.config.hard_tabs,

crates/language/src/language_settings.rs πŸ”—

@@ -1,29 +1,30 @@
 //! Provides `language`-related settings.
 
 use crate::{File, Language, LanguageName, LanguageServerName};
-use anyhow::Result;
 use collections::{FxHashMap, HashMap, HashSet};
 use ec4rs::{
     Properties as EditorconfigProperties,
     property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
 };
 use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
-use gpui::{App, Modifiers, SharedString};
+use gpui::{App, Modifiers};
 use itertools::{Either, Itertools};
-use schemars::{JsonSchema, json_schema};
-use serde::{
-    Deserialize, Deserializer, Serialize,
-    de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor},
-};
+use schemars::json_schema;
 
+pub use settings::{
+    CompletionSettingsContent, EditPredictionProvider, EditPredictionsMode, FormatOnSave,
+    Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LspInsertMode,
+    RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
+};
 use settings::{
-    ParameterizedJsonSchema, Settings, SettingsKey, SettingsLocation, SettingsSources,
-    SettingsStore, SettingsUi,
+    IndentGuideSettingsContent, LanguageTaskSettingsContent, ParameterizedJsonSchema,
+    PrettierSettingsContent, Settings, SettingsContent, SettingsLocation, SettingsStore,
+    SettingsUi,
 };
 use shellexpand;
-use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc};
-use util::schemars::replace_subschema;
-use util::serde::default_true;
+use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
+use util::MergeFrom;
+use util::{ResultExt, schemars::replace_subschema};
 
 /// Initializes the language settings.
 pub fn init(cx: &mut App) {
@@ -63,10 +64,11 @@ pub struct AllLanguageSettings {
     pub defaults: LanguageSettings,
     languages: HashMap<LanguageName, LanguageSettings>,
     pub(crate) file_types: FxHashMap<Arc<str>, GlobSet>,
+    pub(crate) file_globs: FxHashMap<Arc<str>, Vec<String>>,
 }
 
 /// The settings for a particular language.
-#[derive(Debug, Clone, Deserialize)]
+#[derive(Debug, Clone)]
 pub struct LanguageSettings {
     /// How many columns a tab should occupy.
     pub tab_size: NonZeroU32,
@@ -74,7 +76,7 @@ pub struct LanguageSettings {
     /// spaces.
     pub hard_tabs: bool,
     /// How to soft-wrap long lines of text.
-    pub soft_wrap: SoftWrap,
+    pub soft_wrap: settings::SoftWrap,
     /// The column at which to soft-wrap lines, for buffers where soft-wrap
     /// is enabled.
     pub preferred_line_length: u32,
@@ -96,11 +98,11 @@ pub struct LanguageSettings {
     /// when saving it.
     pub ensure_final_newline_on_save: bool,
     /// How to perform a buffer format.
-    pub formatter: SelectedFormatter,
+    pub formatter: settings::SelectedFormatter,
     /// Zed's Prettier integration settings.
     pub prettier: PrettierSettings,
     /// Whether to automatically close JSX tags.
-    pub jsx_tag_auto_close: JsxTagAutoCloseSettings,
+    pub jsx_tag_auto_close: bool,
     /// Whether to use language servers to provide code intelligence.
     pub enable_language_server: bool,
     /// The list of language servers to use (or disable) for this language.
@@ -122,9 +124,9 @@ pub struct LanguageSettings {
     /// scopes.
     pub edit_predictions_disabled_in: Vec<String>,
     /// Whether to show tabs and spaces in the editor.
-    pub show_whitespaces: ShowWhitespaceSetting,
+    pub show_whitespaces: settings::ShowWhitespaceSetting,
     /// Visible characters used to render whitespace when show_whitespaces is enabled.
-    pub whitespace_map: WhitespaceMap,
+    pub whitespace_map: settings::WhitespaceMap,
     /// Whether to start a new line with a comment when a previous line is a comment as well.
     pub extend_comment_on_newline: bool,
     /// Inlay hint related settings.
@@ -147,7 +149,7 @@ pub struct LanguageSettings {
     /// Whether to perform linked edits
     pub linked_edits: bool,
     /// Task configuration for this language.
-    pub tasks: LanguageTaskConfig,
+    pub tasks: LanguageTaskSettings,
     /// Whether to pop the completions menu while typing in an editor without
     /// explicitly requesting it.
     pub show_completions_on_input: bool,
@@ -160,965 +162,210 @@ pub struct LanguageSettings {
     pub debuggers: Vec<String>,
 }
 
-impl LanguageSettings {
-    /// A token representing the rest of the available language servers.
-    const REST_OF_LANGUAGE_SERVERS: &'static str = "...";
-
-    /// Returns the customized list of language servers from the list of
-    /// available language servers.
-    pub fn customized_language_servers(
-        &self,
-        available_language_servers: &[LanguageServerName],
-    ) -> Vec<LanguageServerName> {
-        Self::resolve_language_servers(&self.language_servers, available_language_servers)
-    }
-
-    pub(crate) fn resolve_language_servers(
-        configured_language_servers: &[String],
-        available_language_servers: &[LanguageServerName],
-    ) -> Vec<LanguageServerName> {
-        let (disabled_language_servers, enabled_language_servers): (
-            Vec<LanguageServerName>,
-            Vec<LanguageServerName>,
-        ) = configured_language_servers.iter().partition_map(
-            |language_server| match language_server.strip_prefix('!') {
-                Some(disabled) => Either::Left(LanguageServerName(disabled.to_string().into())),
-                None => Either::Right(LanguageServerName(language_server.clone().into())),
-            },
-        );
-
-        let rest = available_language_servers
-            .iter()
-            .filter(|&available_language_server| {
-                !disabled_language_servers.contains(available_language_server)
-                    && !enabled_language_servers.contains(available_language_server)
-            })
-            .cloned()
-            .collect::<Vec<_>>();
-
-        enabled_language_servers
-            .into_iter()
-            .flat_map(|language_server| {
-                if language_server.0.as_ref() == Self::REST_OF_LANGUAGE_SERVERS {
-                    rest.clone()
-                } else {
-                    vec![language_server]
-                }
-            })
-            .collect::<Vec<_>>()
-    }
-}
-
-/// The provider that supplies edit predictions.
-#[derive(
-    Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
-pub enum EditPredictionProvider {
-    None,
-    #[default]
-    Copilot,
-    Supermaven,
-    Zed,
-}
-
-impl EditPredictionProvider {
-    pub fn is_zed(&self) -> bool {
-        match self {
-            EditPredictionProvider::Zed => true,
-            EditPredictionProvider::None
-            | EditPredictionProvider::Copilot
-            | EditPredictionProvider::Supermaven => false,
-        }
-    }
-}
-
-/// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot)
-/// or [Supermaven](https://supermaven.com).
-#[derive(Clone, Debug, Default, SettingsUi)]
-pub struct EditPredictionSettings {
-    /// The provider that supplies edit predictions.
-    pub provider: EditPredictionProvider,
-    /// A list of globs representing files that edit predictions should be disabled for.
-    /// This list adds to a pre-existing, sensible default set of globs.
-    /// Any additional ones you add are combined with them.
-    #[settings_ui(skip)]
-    pub disabled_globs: Vec<DisabledGlob>,
-    /// Configures how edit predictions are displayed in the buffer.
-    pub mode: EditPredictionsMode,
-    /// Settings specific to GitHub Copilot.
-    pub copilot: CopilotSettings,
-    /// Whether edit predictions are enabled in the assistant panel.
-    /// This setting has no effect if globally disabled.
-    pub enabled_in_text_threads: bool,
-}
-
-impl EditPredictionSettings {
-    /// Returns whether edit predictions are enabled for the given path.
-    pub fn enabled_for_file(&self, file: &Arc<dyn File>, cx: &App) -> bool {
-        !self.disabled_globs.iter().any(|glob| {
-            if glob.is_absolute {
-                file.as_local()
-                    .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx)))
-            } else {
-                glob.matcher.is_match(file.path())
-            }
-        })
-    }
-}
-
-#[derive(Clone, Debug)]
-pub struct DisabledGlob {
-    matcher: GlobMatcher,
-    is_absolute: bool,
-}
-
-/// The mode in which edit predictions should be displayed.
-#[derive(
-    Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
-pub enum EditPredictionsMode {
-    /// If provider supports it, display inline when holding modifier key (e.g., alt).
-    /// Otherwise, eager preview is used.
-    #[serde(alias = "auto")]
-    Subtle,
-    /// Display inline when there are no language server completions available.
-    #[default]
-    #[serde(alias = "eager_preview")]
-    Eager,
-}
-
-#[derive(Clone, Debug, Default, SettingsUi)]
-pub struct CopilotSettings {
-    /// HTTP/HTTPS proxy to use for Copilot.
-    #[settings_ui(skip)]
-    pub proxy: Option<String>,
-    /// Disable certificate verification for proxy (not recommended).
-    pub proxy_no_verify: Option<bool>,
-    /// Enterprise URI for Copilot.
-    #[settings_ui(skip)]
-    pub enterprise_uri: Option<String>,
-}
-
-/// The settings for all languages.
-#[derive(
-    Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey,
-)]
-#[settings_key(None)]
-#[settings_ui(group = "Default Language Settings")]
-pub struct AllLanguageSettingsContent {
-    /// The settings for enabling/disabling features.
-    #[serde(default)]
-    pub features: Option<FeaturesContent>,
-    /// The edit prediction settings.
-    #[serde(default)]
-    pub edit_predictions: Option<EditPredictionSettingsContent>,
-    /// The default language settings.
-    #[serde(flatten)]
-    pub defaults: LanguageSettingsContent,
-    /// The settings for individual languages.
-    #[serde(default)]
-    pub languages: LanguageToSettingsMap,
-    /// Settings for associating file extensions and filenames
-    /// with languages.
-    #[serde(default)]
-    #[settings_ui(skip)]
-    pub file_types: HashMap<Arc<str>, Vec<String>>,
-}
-
-/// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language
-/// names in the keys.
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct LanguageToSettingsMap(pub HashMap<LanguageName, LanguageSettingsContent>);
-
-impl SettingsUi for LanguageToSettingsMap {
-    fn settings_ui_item() -> settings::SettingsUiItem {
-        settings::SettingsUiItem::DynamicMap(settings::SettingsUiItemDynamicMap {
-            item: LanguageSettingsContent::settings_ui_item,
-            defaults_path: &[],
-            determine_items: |settings_value, cx| {
-                use settings::SettingsUiEntryMetaData;
-
-                // todo(settings_ui): We should be using a global LanguageRegistry, but it's not implemented yet
-                _ = cx;
-
-                let Some(settings_language_map) = settings_value.as_object() else {
-                    return Vec::new();
-                };
-                let mut languages = Vec::with_capacity(settings_language_map.len());
-
-                for language_name in settings_language_map.keys().map(gpui::SharedString::from) {
-                    languages.push(SettingsUiEntryMetaData {
-                        title: language_name.clone(),
-                        path: language_name,
-                        // todo(settings_ui): Implement documentation for each language
-                        // ideally based on the language's official docs from extension or builtin info
-                        documentation: None,
-                    });
-                }
-                return languages;
-            },
-        })
-    }
-}
-
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, params, _cx| {
-            let language_settings_content_ref = generator
-                .subschema_for::<LanguageSettingsContent>()
-                .to_value();
-            replace_subschema::<LanguageToSettingsMap>(generator, || json_schema!({
-                "type": "object",
-                "properties": params
-                    .language_names
-                    .iter()
-                    .map(|name| {
-                        (
-                            name.clone(),
-                            language_settings_content_ref.clone(),
-                        )
-                    })
-                    .collect::<serde_json::Map<_, _>>()
-            }))
-        }
-    }
-}
-
-/// Controls how completions are processed for this language.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
-#[serde(rename_all = "snake_case")]
+#[derive(Debug, Clone)]
 pub struct CompletionSettings {
     /// Controls how words are completed.
     /// For large documents, not all words may be fetched for completion.
     ///
     /// Default: `fallback`
-    #[serde(default = "default_words_completion_mode")]
     pub words: WordsCompletionMode,
     /// How many characters has to be in the completions query to automatically show the words-based completions.
     /// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
     ///
     /// Default: 3
-    #[serde(default = "default_3")]
     pub words_min_length: usize,
     /// Whether to fetch LSP completions or not.
     ///
     /// Default: true
-    #[serde(default = "default_true")]
     pub lsp: bool,
     /// When fetching LSP completions, determines how long to wait for a response of a particular server.
     /// When set to 0, waits indefinitely.
     ///
     /// Default: 0
-    #[serde(default)]
     pub lsp_fetch_timeout_ms: u64,
     /// Controls how LSP completions are inserted.
     ///
     /// Default: "replace_suffix"
-    #[serde(default = "default_lsp_insert_mode")]
     pub lsp_insert_mode: LspInsertMode,
 }
 
-/// Controls how document's words are completed.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum WordsCompletionMode {
-    /// Always fetch document's words for completions along with LSP completions.
-    Enabled,
-    /// Only if LSP response errors or times out,
-    /// use document's words to show completions.
-    Fallback,
-    /// Never fetch or complete document's words for completions.
-    /// (Word-based completions can still be queried via a separate action)
-    Disabled,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum LspInsertMode {
-    /// Replaces text before the cursor, using the `insert` range described in the LSP specification.
-    Insert,
-    /// Replaces text before and after the cursor, using the `replace` range described in the LSP specification.
-    Replace,
-    /// Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text,
-    /// and like `"insert"` otherwise.
-    ReplaceSubsequence,
-    /// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like
-    /// `"insert"` otherwise.
-    ReplaceSuffix,
-}
-
-fn default_words_completion_mode() -> WordsCompletionMode {
-    WordsCompletionMode::Fallback
-}
-
-fn default_lsp_insert_mode() -> LspInsertMode {
-    LspInsertMode::ReplaceSuffix
-}
-
-fn default_3() -> usize {
-    3
+impl CompletionSettings {
+    pub fn merge_from(&mut self, src: &Option<CompletionSettingsContent>) {
+        let Some(src) = src else { return };
+        self.words.merge_from(&src.words);
+        self.words_min_length.merge_from(&src.words_min_length);
+        self.lsp.merge_from(&src.lsp);
+        self.lsp_fetch_timeout_ms
+            .merge_from(&src.lsp_fetch_timeout_ms);
+        self.lsp_insert_mode.merge_from(&src.lsp_insert_mode);
+    }
 }
 
-/// The settings for a particular language.
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)]
-#[settings_ui(group = "Default")]
-pub struct LanguageSettingsContent {
-    /// How many columns a tab should occupy.
-    ///
-    /// Default: 4
-    #[serde(default)]
-    #[settings_ui(skip)]
-    pub tab_size: Option<NonZeroU32>,
-    /// Whether to indent lines using tab characters, as opposed to multiple
-    /// spaces.
-    ///
-    /// Default: false
-    #[serde(default)]
-    pub hard_tabs: Option<bool>,
-    /// How to soft-wrap long lines of text.
-    ///
-    /// Default: none
-    #[serde(default)]
-    pub soft_wrap: Option<SoftWrap>,
-    /// The column at which to soft-wrap lines, for buffers where soft-wrap
-    /// is enabled.
-    ///
-    /// Default: 80
-    #[serde(default)]
-    pub preferred_line_length: Option<u32>,
-    /// Whether to show wrap guides in the editor. Setting this to true will
-    /// show a guide at the 'preferred_line_length' value if softwrap is set to
-    /// 'preferred_line_length', and will show any additional guides as specified
-    /// by the 'wrap_guides' setting.
-    ///
-    /// Default: true
-    #[serde(default)]
-    pub show_wrap_guides: Option<bool>,
-    /// Character counts at which to show wrap guides in the editor.
-    ///
-    /// Default: []
-    #[serde(default)]
-    #[settings_ui(skip)]
-    pub wrap_guides: Option<Vec<usize>>,
-    /// Indent guide related settings.
-    #[serde(default)]
-    pub indent_guides: Option<IndentGuideSettings>,
-    /// Whether or not to perform a buffer format before saving.
-    ///
-    /// Default: on
-    #[serde(default)]
-    pub format_on_save: Option<FormatOnSave>,
-    /// Whether or not to remove any trailing whitespace from lines of a buffer
-    /// before saving it.
-    ///
-    /// Default: true
-    #[serde(default)]
-    pub remove_trailing_whitespace_on_save: Option<bool>,
-    /// Whether or not to ensure there's a single newline at the end of a buffer
-    /// when saving it.
-    ///
-    /// Default: true
-    #[serde(default)]
-    pub ensure_final_newline_on_save: Option<bool>,
-    /// How to perform a buffer format.
-    ///
-    /// Default: auto
-    #[serde(default)]
-    #[settings_ui(skip)]
-    pub formatter: Option<SelectedFormatter>,
-    /// Zed's Prettier integration settings.
-    /// Allows to enable/disable formatting with Prettier
-    /// and configure default Prettier, used when no project-level Prettier installation is found.
-    ///
-    /// Default: off
-    #[serde(default)]
-    pub prettier: Option<PrettierSettings>,
-    /// Whether to automatically close JSX tags.
-    #[serde(default)]
-    pub jsx_tag_auto_close: Option<JsxTagAutoCloseSettings>,
-    /// Whether to use language servers to provide code intelligence.
-    ///
-    /// Default: true
-    #[serde(default)]
-    pub enable_language_server: Option<bool>,
-    /// The list of language servers to use (or disable) for this language.
-    ///
-    /// This array should consist of language server IDs, as well as the following
-    /// special tokens:
-    /// - `"!<language_server_id>"` - A language server ID prefixed with a `!` will be disabled.
-    /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language.
-    ///
-    /// Default: ["..."]
-    #[serde(default)]
-    #[settings_ui(skip)]
-    pub language_servers: Option<Vec<String>>,
-    /// Controls where the `editor::Rewrap` action is allowed for this language.
-    ///
-    /// Note: This setting has no effect in Vim mode, as rewrap is already
-    /// allowed everywhere.
-    ///
-    /// Default: "in_comments"
-    #[serde(default)]
-    pub allow_rewrap: Option<RewrapBehavior>,
-    /// Controls whether edit predictions are shown immediately (true)
-    /// or manually by triggering `editor::ShowEditPrediction` (false).
-    ///
-    /// Default: true
-    #[serde(default)]
-    pub show_edit_predictions: Option<bool>,
-    /// Controls whether edit predictions are shown in the given language
-    /// scopes.
-    ///
-    /// Example: ["string", "comment"]
-    ///
-    /// Default: []
-    #[serde(default)]
-    #[settings_ui(skip)]
-    pub edit_predictions_disabled_in: Option<Vec<String>>,
-    /// Whether to show tabs and spaces in the editor.
-    #[serde(default)]
-    pub show_whitespaces: Option<ShowWhitespaceSetting>,
-    /// Visible characters used to render whitespace when show_whitespaces is enabled.
-    ///
-    /// Default: "β€’" for spaces, "β†’" for tabs.
-    #[serde(default)]
-    pub whitespace_map: Option<WhitespaceMap>,
-    /// Whether to start a new line with a comment when a previous line is a comment as well.
-    ///
-    /// Default: true
-    #[serde(default)]
-    pub extend_comment_on_newline: Option<bool>,
-    /// Inlay hint related settings.
-    #[serde(default)]
-    pub inlay_hints: Option<InlayHintSettings>,
-    /// Whether to automatically type closing characters for you. For example,
-    /// when you type (, Zed will automatically add a closing ) at the correct position.
-    ///
-    /// Default: true
-    pub use_autoclose: Option<bool>,
-    /// Whether to automatically surround text with characters for you. For example,
-    /// when you select text and type (, Zed will automatically surround text with ().
-    ///
-    /// Default: true
-    pub use_auto_surround: Option<bool>,
-    /// Controls how the editor handles the autoclosed characters.
-    /// When set to `false`(default), skipping over and auto-removing of the closing characters
-    /// happen only for auto-inserted characters.
-    /// Otherwise(when `true`), the closing characters are always skipped over and auto-removed
-    /// no matter how they were inserted.
-    ///
-    /// Default: false
-    pub always_treat_brackets_as_autoclosed: Option<bool>,
-    /// Whether to use additional LSP queries to format (and amend) the code after
-    /// every "trigger" symbol input, defined by LSP server capabilities.
-    ///
-    /// Default: true
-    pub use_on_type_format: Option<bool>,
-    /// Which code actions to run on save after the formatter.
-    /// These are not run if formatting is off.
-    ///
-    /// Default: {} (or {"source.organizeImports": true} for Go).
-    #[settings_ui(skip)]
-    pub code_actions_on_format: Option<HashMap<String, bool>>,
-    /// Whether to perform linked edits of associated ranges, if the language server supports it.
-    /// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
-    ///
-    /// Default: true
-    pub linked_edits: Option<bool>,
-    /// Whether indentation should be adjusted based on the context whilst typing.
-    ///
-    /// Default: true
-    pub auto_indent: Option<bool>,
-    /// Whether indentation of pasted content should be adjusted based on the context.
+/// The settings for indent guides.
+#[derive(Debug, Clone, PartialEq)]
+pub struct IndentGuideSettings {
+    /// Whether to display indent guides in the editor.
     ///
     /// Default: true
-    pub auto_indent_on_paste: Option<bool>,
-    /// Task configuration for this language.
+    pub enabled: bool,
+    /// The width of the indent guides in pixels, between 1 and 10.
     ///
-    /// Default: {}
-    pub tasks: Option<LanguageTaskConfig>,
-    /// Whether to pop the completions menu while typing in an editor without
-    /// explicitly requesting it.
+    /// Default: 1
+    pub line_width: u32,
+    /// The width of the active indent guide in pixels, between 1 and 10.
     ///
-    /// Default: true
-    pub show_completions_on_input: Option<bool>,
-    /// Whether to display inline and alongside documentation for items in the
-    /// completions menu.
+    /// Default: 1
+    pub active_line_width: u32,
+    /// Determines how indent guides are colored.
     ///
-    /// Default: true
-    pub show_completion_documentation: Option<bool>,
-    /// Controls how completions are processed for this language.
-    pub completions: Option<CompletionSettings>,
-    /// Preferred debuggers for this language.
+    /// Default: Fixed
+    pub coloring: settings::IndentGuideColoring,
+    /// Determines how indent guide backgrounds are colored.
     ///
-    /// Default: []
-    #[settings_ui(skip)]
-    pub debuggers: Option<Vec<String>>,
+    /// Default: Disabled
+    pub background_coloring: settings::IndentGuideBackgroundColoring,
 }
 
-/// The behavior of `editor::Rewrap`.
-#[derive(
-    Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, SettingsUi,
-)]
-#[serde(rename_all = "snake_case")]
-pub enum RewrapBehavior {
-    /// Only rewrap within comments.
-    #[default]
-    InComments,
-    /// Only rewrap within the current selection(s).
-    InSelections,
-    /// Allow rewrapping anywhere.
-    Anywhere,
-}
+impl IndentGuideSettings {
+    pub fn merge_from(&mut self, src: &Option<IndentGuideSettingsContent>) {
+        let Some(src) = src else { return };
 
-/// The contents of the edit prediction settings.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)]
-pub struct EditPredictionSettingsContent {
-    /// A list of globs representing files that edit predictions should be disabled for.
-    /// This list adds to a pre-existing, sensible default set of globs.
-    /// Any additional ones you add are combined with them.
-    #[serde(default)]
-    #[settings_ui(skip)]
-    pub disabled_globs: Option<Vec<String>>,
-    /// The mode used to display edit predictions in the buffer.
-    /// Provider support required.
-    #[serde(default)]
-    pub mode: EditPredictionsMode,
-    /// Settings specific to GitHub Copilot.
-    #[serde(default)]
-    pub copilot: CopilotSettingsContent,
-    /// Whether edit predictions are enabled in the assistant prompt editor.
-    /// This has no effect if globally disabled.
-    #[serde(default = "default_true")]
-    pub enabled_in_text_threads: bool,
+        self.enabled.merge_from(&src.enabled);
+        self.line_width.merge_from(&src.line_width);
+        self.active_line_width.merge_from(&src.active_line_width);
+        self.coloring.merge_from(&src.coloring);
+        self.background_coloring
+            .merge_from(&src.background_coloring);
+    }
 }
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)]
-pub struct CopilotSettingsContent {
-    /// HTTP/HTTPS proxy to use for Copilot.
-    ///
-    /// Default: none
-    #[serde(default)]
-    #[settings_ui(skip)]
-    pub proxy: Option<String>,
-    /// Disable certificate verification for the proxy (not recommended).
-    ///
-    /// Default: false
-    #[serde(default)]
-    pub proxy_no_verify: Option<bool>,
-    /// Enterprise URI for Copilot.
+#[derive(Debug, Clone)]
+pub struct LanguageTaskSettings {
+    /// Extra task variables to set for a particular language.
+    pub variables: HashMap<String, String>,
+    pub enabled: bool,
+    /// Use LSP tasks over Zed language extension ones.
+    /// If no LSP tasks are returned due to error/timeout or regular execution,
+    /// Zed language extension tasks will be used instead.
     ///
-    /// Default: none
-    #[serde(default)]
-    #[settings_ui(skip)]
-    pub enterprise_uri: Option<String>,
-}
-
-/// The settings for enabling/disabling features.
-#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-#[settings_ui(group = "Features")]
-pub struct FeaturesContent {
-    /// Determines which edit prediction provider to use.
-    pub edit_prediction_provider: Option<EditPredictionProvider>,
-}
-
-/// Controls the soft-wrapping behavior in the editor.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-pub enum SoftWrap {
-    /// Prefer a single line generally, unless an overly long line is encountered.
-    None,
-    /// Deprecated: use None instead. Left to avoid breaking existing users' configs.
-    /// Prefer a single line generally, unless an overly long line is encountered.
-    PreferLine,
-    /// Soft wrap lines that exceed the editor width.
-    EditorWidth,
-    /// Soft wrap lines at the preferred line length.
-    PreferredLineLength,
-    /// Soft wrap line at the preferred line length or the editor width (whichever is smaller).
-    Bounded,
-}
-
-/// Controls the behavior of formatting files when they are saved.
-#[derive(Debug, Clone, PartialEq, Eq, SettingsUi)]
-pub enum FormatOnSave {
-    /// Files should be formatted on save.
-    On,
-    /// Files should not be formatted on save.
-    Off,
-    List(FormatterList),
-}
-
-impl JsonSchema for FormatOnSave {
-    fn schema_name() -> Cow<'static, str> {
-        "OnSaveFormatter".into()
-    }
-
-    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
-        let formatter_schema = Formatter::json_schema(generator);
-
-        json_schema!({
-            "oneOf": [
-                {
-                    "type": "array",
-                    "items": formatter_schema
-                },
-                {
-                    "type": "string",
-                    "enum": ["on", "off", "language_server"]
-                },
-                formatter_schema
-            ]
-        })
-    }
+    /// Other Zed tasks will still be shown:
+    /// * Zed task from either of the task config file
+    /// * Zed task from history (e.g. one-off task was spawned before)
+    pub prefer_lsp: bool,
 }
 
-impl Serialize for FormatOnSave {
-    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        match self {
-            Self::On => serializer.serialize_str("on"),
-            Self::Off => serializer.serialize_str("off"),
-            Self::List(list) => list.serialize(serializer),
-        }
+impl LanguageTaskSettings {
+    pub fn merge_from(&mut self, src: &Option<LanguageTaskSettingsContent>) {
+        let Some(src) = src.clone() else {
+            return;
+        };
+        self.variables.extend(src.variables);
+        self.enabled.merge_from(&src.enabled);
+        self.prefer_lsp.merge_from(&src.prefer_lsp);
     }
 }
 
-impl<'de> Deserialize<'de> for FormatOnSave {
-    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        struct FormatDeserializer;
-
-        impl<'d> Visitor<'d> for FormatDeserializer {
-            type Value = FormatOnSave;
+/// Allows to enable/disable formatting with Prettier
+/// and configure default Prettier, used when no project-level Prettier installation is found.
+/// Prettier formatting is disabled by default.
+#[derive(Debug, Clone)]
+pub struct PrettierSettings {
+    /// Enables or disables formatting with Prettier for a given language.
+    pub allowed: bool,
 
-            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
-                formatter.write_str("a valid on-save formatter kind")
-            }
-            fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
-            where
-                E: serde::de::Error,
-            {
-                if v == "on" {
-                    Ok(Self::Value::On)
-                } else if v == "off" {
-                    Ok(Self::Value::Off)
-                } else if v == "language_server" {
-                    Ok(Self::Value::List(FormatterList::Single(
-                        Formatter::LanguageServer { name: None },
-                    )))
-                } else {
-                    let ret: Result<FormatterList, _> =
-                        Deserialize::deserialize(v.into_deserializer());
-                    ret.map(Self::Value::List)
-                }
-            }
-            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
-            where
-                A: MapAccess<'d>,
-            {
-                let ret: Result<FormatterList, _> =
-                    Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
-                ret.map(Self::Value::List)
-            }
-            fn visit_seq<A>(self, map: A) -> Result<Self::Value, A::Error>
-            where
-                A: SeqAccess<'d>,
-            {
-                let ret: Result<FormatterList, _> =
-                    Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
-                ret.map(Self::Value::List)
-            }
-        }
-        deserializer.deserialize_any(FormatDeserializer)
-    }
-}
+    /// Forces Prettier integration to use a specific parser name when formatting files with the language.
+    pub parser: Option<String>,
 
-/// Controls how whitespace should be displayedin the editor.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowWhitespaceSetting {
-    /// Draw whitespace only for the selected text.
-    Selection,
-    /// Do not draw any tabs or spaces.
-    None,
-    /// Draw all invisible symbols.
-    All,
-    /// Draw whitespaces at boundaries only.
-    ///
-    /// For a whitespace to be on a boundary, any of the following conditions need to be met:
-    /// - It is a tab
-    /// - It is adjacent to an edge (start or end)
-    /// - It is adjacent to a whitespace (left or right)
-    Boundary,
-    /// Draw whitespaces only after non-whitespace characters.
-    Trailing,
-}
+    /// Forces Prettier integration to use specific plugins when formatting files with the language.
+    /// The default Prettier will be installed with these plugins.
+    pub plugins: HashSet<String>,
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)]
-pub struct WhitespaceMap {
-    #[serde(default)]
-    pub space: Option<String>,
-    #[serde(default)]
-    pub tab: Option<String>,
+    /// Default Prettier options, in the format as in package.json section for Prettier.
+    /// If project installs Prettier via its package.json, these options will be ignored.
+    pub options: HashMap<String, serde_json::Value>,
 }
 
-impl WhitespaceMap {
-    pub fn space(&self) -> SharedString {
-        self.space
-            .as_ref()
-            .map_or_else(|| SharedString::from("β€’"), |s| SharedString::from(s))
-    }
-
-    pub fn tab(&self) -> SharedString {
-        self.tab
-            .as_ref()
-            .map_or_else(|| SharedString::from("β†’"), |s| SharedString::from(s))
+impl PrettierSettings {
+    pub fn merge_from(&mut self, src: &Option<PrettierSettingsContent>) {
+        let Some(src) = src.clone() else { return };
+        self.allowed.merge_from(&src.allowed);
+        self.parser = src.parser.clone();
+        self.plugins.extend(src.plugins);
+        self.options.extend(src.options);
     }
 }
 
-/// Controls which formatter should be used when formatting code.
-#[derive(Clone, Debug, Default, PartialEq, Eq, SettingsUi)]
-pub enum SelectedFormatter {
-    /// Format files using Zed's Prettier integration (if applicable),
-    /// or falling back to formatting via language server.
-    #[default]
-    Auto,
-    List(FormatterList),
-}
-
-impl JsonSchema for SelectedFormatter {
-    fn schema_name() -> Cow<'static, str> {
-        "Formatter".into()
-    }
-
-    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
-        let formatter_schema = Formatter::json_schema(generator);
-
-        json_schema!({
-            "oneOf": [
-                {
-                    "type": "array",
-                    "items": formatter_schema
-                },
-                {
-                    "type": "string",
-                    "enum": ["auto", "language_server"]
-                },
-                formatter_schema
-            ]
-        })
-    }
-}
+impl LanguageSettings {
+    /// A token representing the rest of the available language servers.
+    const REST_OF_LANGUAGE_SERVERS: &'static str = "...";
 
-impl Serialize for SelectedFormatter {
-    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        match self {
-            SelectedFormatter::Auto => serializer.serialize_str("auto"),
-            SelectedFormatter::List(list) => list.serialize(serializer),
-        }
+    /// Returns the customized list of language servers from the list of
+    /// available language servers.
+    pub fn customized_language_servers(
+        &self,
+        available_language_servers: &[LanguageServerName],
+    ) -> Vec<LanguageServerName> {
+        Self::resolve_language_servers(&self.language_servers, available_language_servers)
     }
-}
 
-impl<'de> Deserialize<'de> for SelectedFormatter {
-    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        struct FormatDeserializer;
+    pub(crate) fn resolve_language_servers(
+        configured_language_servers: &[String],
+        available_language_servers: &[LanguageServerName],
+    ) -> Vec<LanguageServerName> {
+        let (disabled_language_servers, enabled_language_servers): (
+            Vec<LanguageServerName>,
+            Vec<LanguageServerName>,
+        ) = configured_language_servers.iter().partition_map(
+            |language_server| match language_server.strip_prefix('!') {
+                Some(disabled) => Either::Left(LanguageServerName(disabled.to_string().into())),
+                None => Either::Right(LanguageServerName(language_server.clone().into())),
+            },
+        );
 
-        impl<'d> Visitor<'d> for FormatDeserializer {
-            type Value = SelectedFormatter;
+        let rest = available_language_servers
+            .iter()
+            .filter(|&available_language_server| {
+                !disabled_language_servers.contains(available_language_server)
+                    && !enabled_language_servers.contains(available_language_server)
+            })
+            .cloned()
+            .collect::<Vec<_>>();
 
-            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
-                formatter.write_str("a valid formatter kind")
-            }
-            fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
-            where
-                E: serde::de::Error,
-            {
-                if v == "auto" {
-                    Ok(Self::Value::Auto)
-                } else if v == "language_server" {
-                    Ok(Self::Value::List(FormatterList::Single(
-                        Formatter::LanguageServer { name: None },
-                    )))
+        enabled_language_servers
+            .into_iter()
+            .flat_map(|language_server| {
+                if language_server.0.as_ref() == Self::REST_OF_LANGUAGE_SERVERS {
+                    rest.clone()
                 } else {
-                    let ret: Result<FormatterList, _> =
-                        Deserialize::deserialize(v.into_deserializer());
-                    ret.map(SelectedFormatter::List)
+                    vec![language_server]
                 }
-            }
-            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
-            where
-                A: MapAccess<'d>,
-            {
-                let ret: Result<FormatterList, _> =
-                    Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
-                ret.map(SelectedFormatter::List)
-            }
-            fn visit_seq<A>(self, map: A) -> Result<Self::Value, A::Error>
-            where
-                A: SeqAccess<'d>,
-            {
-                let ret: Result<FormatterList, _> =
-                    Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
-                ret.map(SelectedFormatter::List)
-            }
-        }
-        deserializer.deserialize_any(FormatDeserializer)
-    }
-}
-
-/// Controls which formatters should be used when formatting code.
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
-#[serde(untagged)]
-pub enum FormatterList {
-    Single(Formatter),
-    Vec(#[settings_ui(skip)] Vec<Formatter>),
-}
-
-impl Default for FormatterList {
-    fn default() -> Self {
-        Self::Single(Formatter::default())
-    }
-}
-
-impl AsRef<[Formatter]> for FormatterList {
-    fn as_ref(&self) -> &[Formatter] {
-        match &self {
-            Self::Single(single) => slice::from_ref(single),
-            Self::Vec(v) => v,
-        }
+            })
+            .collect::<Vec<_>>()
     }
 }
 
-/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration.
-#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-pub enum Formatter {
-    /// Format code using the current language server.
-    LanguageServer {
-        #[settings_ui(skip)]
-        name: Option<String>,
-    },
-    /// Format code using Zed's Prettier integration.
-    #[default]
-    Prettier,
-    /// Format code using an external command.
-    External {
-        /// The external program to run.
-        #[settings_ui(skip)]
-        command: Arc<str>,
-        /// The arguments to pass to the program.
-        #[settings_ui(skip)]
-        arguments: Option<Arc<[String]>>,
-    },
-    /// Files should be formatted using code actions executed by language servers.
-    CodeActions(#[settings_ui(skip)] HashMap<String, bool>),
-}
-
-/// The settings for indent guides.
-#[derive(
-    Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi,
-)]
-pub struct IndentGuideSettings {
-    /// Whether to display indent guides in the editor.
-    ///
-    /// Default: true
-    #[serde(default = "default_true")]
-    pub enabled: bool,
-    /// The width of the indent guides in pixels, between 1 and 10.
-    ///
-    /// Default: 1
-    #[serde(default = "line_width")]
-    pub line_width: u32,
-    /// The width of the active indent guide in pixels, between 1 and 10.
-    ///
-    /// Default: 1
-    #[serde(default = "active_line_width")]
-    pub active_line_width: u32,
-    /// Determines how indent guides are colored.
-    ///
-    /// Default: Fixed
-    #[serde(default)]
-    pub coloring: IndentGuideColoring,
-    /// Determines how indent guide backgrounds are colored.
-    ///
-    /// Default: Disabled
-    #[serde(default)]
-    pub background_coloring: IndentGuideBackgroundColoring,
-}
-
-fn line_width() -> u32 {
-    1
-}
-
-fn active_line_width() -> u32 {
-    line_width()
-}
-
-/// Determines how indent guides are colored.
-#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum IndentGuideColoring {
-    /// Do not render any lines for indent guides.
-    Disabled,
-    /// Use the same color for all indentation levels.
-    #[default]
-    Fixed,
-    /// Use a different color for each indentation level.
-    IndentAware,
-}
-
-/// Determines how indent guide backgrounds are colored.
-#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum IndentGuideBackgroundColoring {
-    /// Do not render any background for indent guides.
-    #[default]
-    Disabled,
-    /// Use a different color for each indentation level.
-    IndentAware,
-}
-
-/// The settings for inlay hints.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
+// The settings for inlay hints.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub struct InlayHintSettings {
     /// Global switch to toggle hints on and off.
     ///
     /// Default: false
-    #[serde(default)]
     pub enabled: bool,
     /// Global switch to toggle inline values on and off when debugging.
     ///
     /// Default: true
-    #[serde(default = "default_true")]
     pub show_value_hints: bool,
     /// Whether type hints should be shown.
     ///
     /// Default: true
-    #[serde(default = "default_true")]
     pub show_type_hints: bool,
     /// Whether parameter hints should be shown.
     ///
     /// Default: true
-    #[serde(default = "default_true")]
     pub show_parameter_hints: bool,
     /// Whether other hints should be shown.
     ///
     /// Default: true
-    #[serde(default = "default_true")]
     pub show_other_hints: bool,
     /// Whether to show a background for inlay hints.
     ///

crates/language_model/Cargo.toml πŸ”—

@@ -35,6 +35,7 @@ proto.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
+settings.workspace = true
 smol.workspace = true
 telemetry_events.workspace = true
 thiserror.workspace = true

crates/language_model/src/language_model.rs πŸ”—

@@ -21,6 +21,7 @@ use open_router::OpenRouterError;
 use parking_lot::Mutex;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize, de::DeserializeOwned};
+pub use settings::LanguageModelCacheConfiguration;
 use std::ops::{Add, Sub};
 use std::str::FromStr;
 use std::sync::Arc;
@@ -62,14 +63,6 @@ pub fn init_settings(cx: &mut App) {
     registry::init(cx);
 }
 
-/// Configuration for caching language model messages.
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct LanguageModelCacheConfiguration {
-    pub max_cache_anchors: usize,
-    pub should_speculate: bool,
-    pub min_total_token: u64,
-}
-
 /// A completion event from a language model.
 #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
 pub enum LanguageModelCompletionEvent {

crates/language_models/src/provider/anthropic.rs πŸ”—

@@ -18,8 +18,6 @@ use language_model::{
     LanguageModelToolResultContent, MessageContent, RateLimiter, Role,
 };
 use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
 use std::pin::Pin;
 use std::str::FromStr;
@@ -30,6 +28,8 @@ use ui::{Icon, IconName, List, Tooltip, prelude::*};
 use util::{ResultExt, truncate_and_trailoff};
 use zed_env_vars::{EnvVar, env_var};
 
+pub use settings::AnthropicAvailableModel as AvailableModel;
+
 const PROVIDER_ID: LanguageModelProviderId = language_model::ANTHROPIC_PROVIDER_ID;
 const PROVIDER_NAME: LanguageModelProviderName = language_model::ANTHROPIC_PROVIDER_NAME;
 
@@ -40,55 +40,6 @@ pub struct AnthropicSettings {
     pub available_models: Vec<AvailableModel>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc
-    pub name: String,
-    /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel.
-    pub display_name: Option<String>,
-    /// The model's context window size.
-    pub max_tokens: u64,
-    /// A model `name` to substitute when calling tools, in case the primary model doesn't support tool calling.
-    pub tool_override: Option<String>,
-    /// Configuration of Anthropic's caching API.
-    pub cache_configuration: Option<LanguageModelCacheConfiguration>,
-    pub max_output_tokens: Option<u64>,
-    pub default_temperature: Option<f32>,
-    #[serde(default)]
-    pub extra_beta_headers: Vec<String>,
-    /// The model's mode (e.g. thinking)
-    pub mode: Option<ModelMode>,
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-#[serde(tag = "type", rename_all = "lowercase")]
-pub enum ModelMode {
-    #[default]
-    Default,
-    Thinking {
-        /// The maximum number of tokens to use for reasoning. Must be lower than the model's `max_output_tokens`.
-        budget_tokens: Option<u32>,
-    },
-}
-
-impl From<ModelMode> for AnthropicModelMode {
-    fn from(value: ModelMode) -> Self {
-        match value {
-            ModelMode::Default => AnthropicModelMode::Default,
-            ModelMode::Thinking { budget_tokens } => AnthropicModelMode::Thinking { budget_tokens },
-        }
-    }
-}
-
-impl From<AnthropicModelMode> for ModelMode {
-    fn from(value: AnthropicModelMode) -> Self {
-        match value {
-            AnthropicModelMode::Default => ModelMode::Default,
-            AnthropicModelMode::Thinking { budget_tokens } => ModelMode::Thinking { budget_tokens },
-        }
-    }
-}
-
 pub struct AnthropicLanguageModelProvider {
     http_client: Arc<dyn HttpClient>,
     state: gpui::Entity<State>,
@@ -237,7 +188,7 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
                     max_output_tokens: model.max_output_tokens,
                     default_temperature: model.default_temperature,
                     extra_beta_headers: model.extra_beta_headers.clone(),
-                    mode: model.mode.clone().unwrap_or_default().into(),
+                    mode: model.mode.unwrap_or_default().into(),
                 },
             );
         }

crates/language_models/src/provider/bedrock.rs πŸ”—

@@ -42,7 +42,7 @@ use language_model::{
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use settings::{Settings, SettingsStore};
+use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore};
 use smol::lock::OnceCell;
 use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};
 use theme::ThemeSettings;
@@ -83,15 +83,14 @@ pub enum BedrockAuthMethod {
     Automatic,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    pub name: String,
-    pub display_name: Option<String>,
-    pub max_tokens: u64,
-    pub cache_configuration: Option<LanguageModelCacheConfiguration>,
-    pub max_output_tokens: Option<u64>,
-    pub default_temperature: Option<f32>,
-    pub mode: Option<ModelMode>,
+impl From<settings::BedrockAuthMethodContent> for BedrockAuthMethod {
+    fn from(value: settings::BedrockAuthMethodContent) -> Self {
+        match value {
+            settings::BedrockAuthMethodContent::SingleSignOn => BedrockAuthMethod::SingleSignOn,
+            settings::BedrockAuthMethodContent::Automatic => BedrockAuthMethod::Automatic,
+            settings::BedrockAuthMethodContent::NamedProfile => BedrockAuthMethod::NamedProfile,
+        }
+    }
 }
 
 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]

crates/language_models/src/provider/cloud.rs πŸ”—

@@ -32,6 +32,8 @@ use release_channel::AppVersion;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize, de::DeserializeOwned};
 use settings::SettingsStore;
+pub use settings::ZedDotDevAvailableModel as AvailableModel;
+pub use settings::ZedDotDevAvailableProvider as AvailableProvider;
 use smol::io::{AsyncReadExt, BufReader};
 use std::pin::Pin;
 use std::str::FromStr as _;
@@ -52,42 +54,6 @@ const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVI
 pub struct ZedDotDevSettings {
     pub available_models: Vec<AvailableModel>,
 }
-
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "lowercase")]
-pub enum AvailableProvider {
-    Anthropic,
-    OpenAi,
-    Google,
-}
-
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    /// The provider of the language model.
-    pub provider: AvailableProvider,
-    /// The model's name in the provider's API. e.g. claude-3-5-sonnet-20240620
-    pub name: String,
-    /// The name displayed in the UI, such as in the assistant panel model dropdown menu.
-    pub display_name: Option<String>,
-    /// The size of the context window, indicating the maximum number of tokens the model can process.
-    pub max_tokens: usize,
-    /// The maximum number of output tokens allowed by the model.
-    pub max_output_tokens: Option<u64>,
-    /// The maximum number of completion tokens allowed by the model (o1-* only)
-    pub max_completion_tokens: Option<u64>,
-    /// Override this model with a different Anthropic model for tool calls.
-    pub tool_override: Option<String>,
-    /// Indicates whether this custom model supports caching.
-    pub cache_configuration: Option<LanguageModelCacheConfiguration>,
-    /// The default temperature to use for this model.
-    pub default_temperature: Option<f32>,
-    /// Any extra beta headers to provide when using the model.
-    #[serde(default)]
-    pub extra_beta_headers: Vec<String>,
-    /// The model's mode (e.g. thinking)
-    pub mode: Option<ModelMode>,
-}
-
 #[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
 #[serde(tag = "type", rename_all = "lowercase")]
 pub enum ModelMode {

crates/language_models/src/provider/deepseek.rs πŸ”—

@@ -16,8 +16,7 @@ use language_model::{
     LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
     RateLimiter, Role, StopReason, TokenUsage,
 };
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+pub use settings::DeepseekAvailableModel as AvailableModel;
 use settings::{Settings, SettingsStore};
 use std::pin::Pin;
 use std::str::FromStr;
@@ -47,15 +46,6 @@ pub struct DeepSeekSettings {
     pub api_url: String,
     pub available_models: Vec<AvailableModel>,
 }
-
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    pub name: String,
-    pub display_name: Option<String>,
-    pub max_tokens: u64,
-    pub max_output_tokens: Option<u64>,
-}
-
 pub struct DeepSeekLanguageModelProvider {
     http_client: Arc<dyn HttpClient>,
     state: Entity<State>,

crates/language_models/src/provider/google.rs πŸ”—

@@ -24,6 +24,7 @@ use language_model::{
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+pub use settings::GoogleAvailableModel as AvailableModel;
 use settings::{Settings, SettingsStore};
 use std::pin::Pin;
 use std::sync::{
@@ -60,32 +61,6 @@ pub enum ModelMode {
     },
 }
 
-impl From<ModelMode> for GoogleModelMode {
-    fn from(value: ModelMode) -> Self {
-        match value {
-            ModelMode::Default => GoogleModelMode::Default,
-            ModelMode::Thinking { budget_tokens } => GoogleModelMode::Thinking { budget_tokens },
-        }
-    }
-}
-
-impl From<GoogleModelMode> for ModelMode {
-    fn from(value: GoogleModelMode) -> Self {
-        match value {
-            GoogleModelMode::Default => ModelMode::Default,
-            GoogleModelMode::Thinking { budget_tokens } => ModelMode::Thinking { budget_tokens },
-        }
-    }
-}
-
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    name: String,
-    display_name: Option<String>,
-    max_tokens: u64,
-    mode: Option<ModelMode>,
-}
-
 pub struct GoogleLanguageModelProvider {
     http_client: Arc<dyn HttpClient>,
     state: gpui::Entity<State>,
@@ -234,7 +209,7 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
                     name: model.name.clone(),
                     display_name: model.display_name.clone(),
                     max_tokens: model.max_tokens,
-                    mode: model.mode.unwrap_or_default().into(),
+                    mode: model.mode.unwrap_or_default(),
                 },
             );
         }

crates/language_models/src/provider/lmstudio.rs πŸ”—

@@ -15,8 +15,7 @@ use language_model::{
     LanguageModelRequest, RateLimiter, Role,
 };
 use lmstudio::{ModelType, get_models};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+pub use settings::LmStudioAvailableModel as AvailableModel;
 use settings::{Settings, SettingsStore};
 use std::pin::Pin;
 use std::str::FromStr;
@@ -40,15 +39,6 @@ pub struct LmStudioSettings {
     pub available_models: Vec<AvailableModel>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    pub name: String,
-    pub display_name: Option<String>,
-    pub max_tokens: u64,
-    pub supports_tool_calls: bool,
-    pub supports_images: bool,
-}
-
 pub struct LmStudioLanguageModelProvider {
     http_client: Arc<dyn HttpClient>,
     state: gpui::Entity<State>,

crates/language_models/src/provider/mistral.rs πŸ”—

@@ -15,8 +15,7 @@ use language_model::{
     RateLimiter, Role, StopReason, TokenUsage,
 };
 use mistral::{MISTRAL_API_URL, StreamResponse};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+pub use settings::MistralAvailableModel as AvailableModel;
 use settings::{Settings, SettingsStore};
 use std::collections::HashMap;
 use std::pin::Pin;
@@ -42,18 +41,6 @@ pub struct MistralSettings {
     pub available_models: Vec<AvailableModel>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    pub name: String,
-    pub display_name: Option<String>,
-    pub max_tokens: u64,
-    pub max_output_tokens: Option<u64>,
-    pub max_completion_tokens: Option<u64>,
-    pub supports_tools: Option<bool>,
-    pub supports_images: Option<bool>,
-    pub supports_thinking: Option<bool>,
-}
-
 pub struct MistralLanguageModelProvider {
     http_client: Arc<dyn HttpClient>,
     state: gpui::Entity<State>,

crates/language_models/src/provider/ollama.rs πŸ”—

@@ -13,12 +13,10 @@ use language_model::{
 };
 use menu;
 use ollama::{
-    ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OLLAMA_API_URL,
-    OllamaFunctionCall, OllamaFunctionTool, OllamaToolCall, get_models, show_model,
-    stream_chat_completion,
+    ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, OLLAMA_API_URL, OllamaFunctionCall,
+    OllamaFunctionTool, OllamaToolCall, get_models, show_model, stream_chat_completion,
 };
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+pub use settings::OllamaAvailableModel as AvailableModel;
 use settings::{Settings, SettingsStore, update_settings_file};
 use std::pin::Pin;
 use std::sync::LazyLock;
@@ -48,24 +46,6 @@ pub struct OllamaSettings {
     pub available_models: Vec<AvailableModel>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    /// The model name in the Ollama API (e.g. "llama3.2:latest")
-    pub name: String,
-    /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel.
-    pub display_name: Option<String>,
-    /// The Context Length parameter to the model (aka num_ctx or n_ctx)
-    pub max_tokens: u64,
-    /// The number of seconds to keep the connection open after the last request
-    pub keep_alive: Option<KeepAlive>,
-    /// Whether the model supports tools
-    pub supports_tools: Option<bool>,
-    /// Whether the model supports vision
-    pub supports_images: Option<bool>,
-    /// Whether to enable think mode
-    pub supports_thinking: Option<bool>,
-}
-
 pub struct OllamaLanguageModelProvider {
     http_client: Arc<dyn HttpClient>,
     state: gpui::Entity<State>,
@@ -703,15 +683,13 @@ impl ConfigurationView {
         let current_url = OllamaLanguageModelProvider::api_url(cx);
         if !api_url.is_empty() && &api_url != &current_url {
             let fs = <dyn Fs>::global(cx);
-            update_settings_file::<AllLanguageModelSettings>(fs, cx, move |settings, _| {
-                if let Some(settings) = settings.ollama.as_mut() {
-                    settings.api_url = Some(api_url);
-                } else {
-                    settings.ollama = Some(crate::settings::OllamaSettingsContent {
-                        api_url: Some(api_url),
-                        available_models: None,
-                    });
-                }
+            update_settings_file(fs, cx, move |settings, _| {
+                settings
+                    .language_models
+                    .get_or_insert_default()
+                    .ollama
+                    .get_or_insert_default()
+                    .api_url = Some(api_url);
             });
         }
     }
@@ -720,8 +698,12 @@ impl ConfigurationView {
         self.api_url_editor
             .update(cx, |input, cx| input.set_text("", window, cx));
         let fs = <dyn Fs>::global(cx);
-        update_settings_file::<AllLanguageModelSettings>(fs, cx, |settings, _cx| {
-            if let Some(settings) = settings.ollama.as_mut() {
+        update_settings_file(fs, cx, |settings, _cx| {
+            if let Some(settings) = settings
+                .language_models
+                .as_mut()
+                .and_then(|models| models.ollama.as_mut())
+            {
                 settings.api_url = Some(OLLAMA_API_URL.into());
             }
         });

crates/language_models/src/provider/open_ai.rs πŸ”—

@@ -15,9 +15,7 @@ use menu;
 use open_ai::{
     ImageUrl, Model, OPEN_AI_API_URL, ReasoningEffort, ResponseStreamEvent, stream_completion,
 };
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
+use settings::{OpenAiAvailableModel as AvailableModel, Settings, SettingsStore};
 use std::pin::Pin;
 use std::str::FromStr as _;
 use std::sync::{Arc, LazyLock};
@@ -41,16 +39,6 @@ pub struct OpenAiSettings {
     pub available_models: Vec<AvailableModel>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    pub name: String,
-    pub display_name: Option<String>,
-    pub max_tokens: u64,
-    pub max_output_tokens: Option<u64>,
-    pub max_completion_tokens: Option<u64>,
-    pub reasoning_effort: Option<ReasoningEffort>,
-}
-
 pub struct OpenAiLanguageModelProvider {
     http_client: Arc<dyn HttpClient>,
     state: gpui::Entity<State>,

crates/language_models/src/provider/open_ai_compatible.rs πŸ”—

@@ -11,8 +11,6 @@ use language_model::{
 };
 use menu;
 use open_ai::{ResponseStreamEvent, stream_completion};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
 use std::sync::Arc;
 use ui::{ElevationIndex, Tooltip, prelude::*};
@@ -22,6 +20,8 @@ use zed_env_vars::EnvVar;
 
 use crate::api_key::ApiKeyState;
 use crate::provider::open_ai::{OpenAiEventMapper, into_open_ai};
+pub use settings::OpenAiCompatibleAvailableModel as AvailableModel;
+pub use settings::OpenAiCompatibleModelCapabilities as ModelCapabilities;
 
 #[derive(Default, Clone, Debug, PartialEq)]
 pub struct OpenAiCompatibleSettings {
@@ -29,36 +29,6 @@ pub struct OpenAiCompatibleSettings {
     pub available_models: Vec<AvailableModel>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    pub name: String,
-    pub display_name: Option<String>,
-    pub max_tokens: u64,
-    pub max_output_tokens: Option<u64>,
-    pub max_completion_tokens: Option<u64>,
-    #[serde(default)]
-    pub capabilities: ModelCapabilities,
-}
-
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct ModelCapabilities {
-    pub tools: bool,
-    pub images: bool,
-    pub parallel_tool_calls: bool,
-    pub prompt_cache_key: bool,
-}
-
-impl Default for ModelCapabilities {
-    fn default() -> Self {
-        Self {
-            tools: true,
-            images: false,
-            parallel_tool_calls: false,
-            prompt_cache_key: false,
-        }
-    }
-}
-
 pub struct OpenAiCompatibleLanguageModelProvider {
     id: LanguageModelProviderId,
     name: LanguageModelProviderName,

crates/language_models/src/provider/open_router.rs πŸ”—

@@ -14,12 +14,9 @@ use language_model::{
     LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
 };
 use open_router::{
-    Model, ModelMode as OpenRouterModelMode, OPEN_ROUTER_API_URL, Provider, ResponseStreamEvent,
-    list_models,
+    Model, ModelMode as OpenRouterModelMode, OPEN_ROUTER_API_URL, ResponseStreamEvent, list_models,
 };
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
+use settings::{OpenRouterAvailableModel as AvailableModel, Settings, SettingsStore};
 use std::pin::Pin;
 use std::str::FromStr as _;
 use std::sync::{Arc, LazyLock};
@@ -42,51 +39,6 @@ pub struct OpenRouterSettings {
     pub available_models: Vec<AvailableModel>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    pub name: String,
-    pub display_name: Option<String>,
-    pub max_tokens: u64,
-    pub max_output_tokens: Option<u64>,
-    pub max_completion_tokens: Option<u64>,
-    pub supports_tools: Option<bool>,
-    pub supports_images: Option<bool>,
-    pub mode: Option<ModelMode>,
-    pub provider: Option<Provider>,
-}
-
-#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-#[serde(tag = "type", rename_all = "lowercase")]
-pub enum ModelMode {
-    #[default]
-    Default,
-    Thinking {
-        budget_tokens: Option<u32>,
-    },
-}
-
-impl From<ModelMode> for OpenRouterModelMode {
-    fn from(value: ModelMode) -> Self {
-        match value {
-            ModelMode::Default => OpenRouterModelMode::Default,
-            ModelMode::Thinking { budget_tokens } => {
-                OpenRouterModelMode::Thinking { budget_tokens }
-            }
-        }
-    }
-}
-
-impl From<OpenRouterModelMode> for ModelMode {
-    fn from(value: OpenRouterModelMode) -> Self {
-        match value {
-            OpenRouterModelMode::Default => ModelMode::Default,
-            OpenRouterModelMode::Thinking { budget_tokens } => {
-                ModelMode::Thinking { budget_tokens }
-            }
-        }
-    }
-}
-
 pub struct OpenRouterLanguageModelProvider {
     http_client: Arc<dyn HttpClient>,
     state: gpui::Entity<State>,
@@ -259,7 +211,7 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider {
                 max_tokens: model.max_tokens,
                 supports_tools: model.supports_tools,
                 supports_images: model.supports_images,
-                mode: model.mode.clone().unwrap_or_default().into(),
+                mode: model.mode.unwrap_or_default(),
                 provider: model.provider.clone(),
             });
         }

crates/language_models/src/provider/vercel.rs πŸ”—

@@ -10,8 +10,7 @@ use language_model::{
     LanguageModelToolChoice, RateLimiter, Role,
 };
 use open_ai::ResponseStreamEvent;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+pub use settings::VercelAvailableModel as AvailableModel;
 use settings::{Settings, SettingsStore};
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
@@ -29,21 +28,12 @@ const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new(
 const API_KEY_ENV_VAR_NAME: &str = "VERCEL_API_KEY";
 static API_KEY_ENV_VAR: LazyLock<EnvVar> = env_var!(API_KEY_ENV_VAR_NAME);
 
-#[derive(Default, Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
 pub struct VercelSettings {
     pub api_url: String,
     pub available_models: Vec<AvailableModel>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    pub name: String,
-    pub display_name: Option<String>,
-    pub max_tokens: u64,
-    pub max_output_tokens: Option<u64>,
-    pub max_completion_tokens: Option<u64>,
-}
-
 pub struct VercelLanguageModelProvider {
     http_client: Arc<dyn HttpClient>,
     state: gpui::Entity<State>,

crates/language_models/src/provider/x_ai.rs πŸ”—

@@ -10,8 +10,7 @@ use language_model::{
     LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, Role,
 };
 use open_ai::ResponseStreamEvent;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+pub use settings::XaiAvailableModel as AvailableModel;
 use settings::{Settings, SettingsStore};
 use std::sync::{Arc, LazyLock};
 use strum::IntoEnumIterator;
@@ -35,15 +34,6 @@ pub struct XAiSettings {
     pub available_models: Vec<AvailableModel>,
 }
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct AvailableModel {
-    pub name: String,
-    pub display_name: Option<String>,
-    pub max_tokens: u64,
-    pub max_output_tokens: Option<u64>,
-    pub max_completion_tokens: Option<u64>,
-}
-
 pub struct XAiLanguageModelProvider {
     http_client: Arc<dyn HttpClient>,
     state: gpui::Entity<State>,

crates/language_models/src/settings.rs πŸ”—

@@ -1,27 +1,16 @@
 use std::sync::Arc;
 
-use anyhow::Result;
 use collections::HashMap;
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::Settings;
+use util::MergeFrom;
 
 use crate::provider::{
-    self,
-    anthropic::AnthropicSettings,
-    bedrock::AmazonBedrockSettings,
-    cloud::{self, ZedDotDevSettings},
-    deepseek::DeepSeekSettings,
-    google::GoogleSettings,
-    lmstudio::LmStudioSettings,
-    mistral::MistralSettings,
-    ollama::OllamaSettings,
-    open_ai::OpenAiSettings,
-    open_ai_compatible::OpenAiCompatibleSettings,
-    open_router::OpenRouterSettings,
-    vercel::VercelSettings,
-    x_ai::XAiSettings,
+    anthropic::AnthropicSettings, bedrock::AmazonBedrockSettings, cloud::ZedDotDevSettings,
+    deepseek::DeepSeekSettings, google::GoogleSettings, lmstudio::LmStudioSettings,
+    mistral::MistralSettings, ollama::OllamaSettings, open_ai::OpenAiSettings,
+    open_ai_compatible::OpenAiCompatibleSettings, open_router::OpenRouterSettings,
+    vercel::VercelSettings, x_ai::XAiSettings,
 };
 
 /// Initializes the language model settings.
@@ -29,7 +18,6 @@ pub fn init_settings(cx: &mut App) {
     AllLanguageModelSettings::register(cx);
 }
 
-#[derive(Default)]
 pub struct AllLanguageModelSettings {
     pub anthropic: AnthropicSettings,
     pub bedrock: AmazonBedrockSettings,
@@ -46,281 +34,197 @@ pub struct AllLanguageModelSettings {
     pub zed_dot_dev: ZedDotDevSettings,
 }
 
-#[derive(
-    Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, SettingsUi, SettingsKey,
-)]
-#[settings_key(key = "language_models")]
-pub struct AllLanguageModelSettingsContent {
-    pub anthropic: Option<AnthropicSettingsContent>,
-    pub bedrock: Option<AmazonBedrockSettingsContent>,
-    pub deepseek: Option<DeepseekSettingsContent>,
-    pub google: Option<GoogleSettingsContent>,
-    pub lmstudio: Option<LmStudioSettingsContent>,
-    pub mistral: Option<MistralSettingsContent>,
-    pub ollama: Option<OllamaSettingsContent>,
-    pub open_router: Option<OpenRouterSettingsContent>,
-    pub openai: Option<OpenAiSettingsContent>,
-    pub openai_compatible: Option<HashMap<Arc<str>, OpenAiCompatibleSettingsContent>>,
-    pub vercel: Option<VercelSettingsContent>,
-    pub x_ai: Option<XAiSettingsContent>,
-    #[serde(rename = "zed.dev")]
-    pub zed_dot_dev: Option<ZedDotDevSettingsContent>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct AnthropicSettingsContent {
-    pub api_url: Option<String>,
-    pub available_models: Option<Vec<provider::anthropic::AvailableModel>>,
-}
-
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct AmazonBedrockSettingsContent {
-    available_models: Option<Vec<provider::bedrock::AvailableModel>>,
-    endpoint_url: Option<String>,
-    region: Option<String>,
-    profile: Option<String>,
-    authentication_method: Option<provider::bedrock::BedrockAuthMethod>,
-}
-
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct OllamaSettingsContent {
-    pub api_url: Option<String>,
-    pub available_models: Option<Vec<provider::ollama::AvailableModel>>,
-}
-
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct LmStudioSettingsContent {
-    pub api_url: Option<String>,
-    pub available_models: Option<Vec<provider::lmstudio::AvailableModel>>,
-}
-
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct DeepseekSettingsContent {
-    pub api_url: Option<String>,
-    pub available_models: Option<Vec<provider::deepseek::AvailableModel>>,
-}
-
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct MistralSettingsContent {
-    pub api_url: Option<String>,
-    pub available_models: Option<Vec<provider::mistral::AvailableModel>>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct OpenAiSettingsContent {
-    pub api_url: Option<String>,
-    pub available_models: Option<Vec<provider::open_ai::AvailableModel>>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct OpenAiCompatibleSettingsContent {
-    pub api_url: String,
-    pub available_models: Vec<provider::open_ai_compatible::AvailableModel>,
-}
-
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct VercelSettingsContent {
-    pub api_url: Option<String>,
-    pub available_models: Option<Vec<provider::vercel::AvailableModel>>,
-}
-
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct GoogleSettingsContent {
-    pub api_url: Option<String>,
-    pub available_models: Option<Vec<provider::google::AvailableModel>>,
-}
-
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct XAiSettingsContent {
-    pub api_url: Option<String>,
-    pub available_models: Option<Vec<provider::x_ai::AvailableModel>>,
-}
-
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct ZedDotDevSettingsContent {
-    available_models: Option<Vec<cloud::AvailableModel>>,
-}
-
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct OpenRouterSettingsContent {
-    pub api_url: Option<String>,
-    pub available_models: Option<Vec<provider::open_router::AvailableModel>>,
-}
-
 impl settings::Settings for AllLanguageModelSettings {
     const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
 
-    type FileContent = AllLanguageModelSettingsContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        fn merge<T>(target: &mut T, value: Option<T>) {
-            if let Some(value) = value {
-                *target = value;
-            }
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let language_models = content.language_models.clone().unwrap();
+        let anthropic = language_models.anthropic.unwrap();
+        let bedrock = language_models.bedrock.unwrap();
+        let deepseek = language_models.deepseek.unwrap();
+        let google = language_models.google.unwrap();
+        let lmstudio = language_models.lmstudio.unwrap();
+        let mistral = language_models.mistral.unwrap();
+        let ollama = language_models.ollama.unwrap();
+        let open_router = language_models.open_router.unwrap();
+        let openai = language_models.openai.unwrap();
+        let openai_compatible = language_models.openai_compatible.unwrap();
+        let vercel = language_models.vercel.unwrap();
+        let x_ai = language_models.x_ai.unwrap();
+        let zed_dot_dev = language_models.zed_dot_dev.unwrap();
+        Self {
+            anthropic: AnthropicSettings {
+                api_url: anthropic.api_url.unwrap(),
+                available_models: anthropic.available_models.unwrap_or_default(),
+            },
+            bedrock: AmazonBedrockSettings {
+                available_models: bedrock.available_models.unwrap_or_default(),
+                region: bedrock.region,
+                endpoint: bedrock.endpoint_url, // todo(should be api_url)
+                profile_name: bedrock.profile,
+                role_arn: None, // todo(was never a setting for this...)
+                authentication_method: bedrock.authentication_method.map(Into::into),
+            },
+            deepseek: DeepSeekSettings {
+                api_url: deepseek.api_url.unwrap(),
+                available_models: deepseek.available_models.unwrap_or_default(),
+            },
+            google: GoogleSettings {
+                api_url: google.api_url.unwrap(),
+                available_models: google.available_models.unwrap_or_default(),
+            },
+            lmstudio: LmStudioSettings {
+                api_url: lmstudio.api_url.unwrap(),
+                available_models: lmstudio.available_models.unwrap_or_default(),
+            },
+            mistral: MistralSettings {
+                api_url: mistral.api_url.unwrap(),
+                available_models: mistral.available_models.unwrap_or_default(),
+            },
+            ollama: OllamaSettings {
+                api_url: ollama.api_url.unwrap(),
+                available_models: ollama.available_models.unwrap_or_default(),
+            },
+            open_router: OpenRouterSettings {
+                api_url: open_router.api_url.unwrap(),
+                available_models: open_router.available_models.unwrap_or_default(),
+            },
+            openai: OpenAiSettings {
+                api_url: openai.api_url.unwrap(),
+                available_models: openai.available_models.unwrap_or_default(),
+            },
+            openai_compatible: openai_compatible
+                .into_iter()
+                .map(|(key, value)| {
+                    (
+                        key,
+                        OpenAiCompatibleSettings {
+                            api_url: value.api_url,
+                            available_models: value.available_models,
+                        },
+                    )
+                })
+                .collect(),
+            vercel: VercelSettings {
+                api_url: vercel.api_url.unwrap(),
+                available_models: vercel.available_models.unwrap_or_default(),
+            },
+            x_ai: XAiSettings {
+                api_url: x_ai.api_url.unwrap(),
+                available_models: x_ai.available_models.unwrap_or_default(),
+            },
+            zed_dot_dev: ZedDotDevSettings {
+                available_models: zed_dot_dev.available_models.unwrap_or_default(),
+            },
         }
+    }
 
-        let mut settings = AllLanguageModelSettings::default();
-
-        for value in sources.defaults_and_customizations() {
-            // Anthropic
-            let anthropic = value.anthropic.clone();
-            merge(
-                &mut settings.anthropic.api_url,
-                anthropic.as_ref().and_then(|s| s.api_url.clone()),
-            );
-            merge(
-                &mut settings.anthropic.available_models,
-                anthropic.as_ref().and_then(|s| s.available_models.clone()),
-            );
-
-            // Bedrock
-            let bedrock = value.bedrock.clone();
-            merge(
-                &mut settings.bedrock.profile_name,
-                bedrock.as_ref().map(|s| s.profile.clone()),
-            );
-            merge(
-                &mut settings.bedrock.authentication_method,
-                bedrock.as_ref().map(|s| s.authentication_method.clone()),
-            );
-            merge(
-                &mut settings.bedrock.region,
-                bedrock.as_ref().map(|s| s.region.clone()),
-            );
-            merge(
-                &mut settings.bedrock.endpoint,
-                bedrock.as_ref().map(|s| s.endpoint_url.clone()),
-            );
-
-            // Ollama
-            let ollama = value.ollama.clone();
-
-            merge(
-                &mut settings.ollama.api_url,
-                value.ollama.as_ref().and_then(|s| s.api_url.clone()),
-            );
-            merge(
-                &mut settings.ollama.available_models,
-                ollama.as_ref().and_then(|s| s.available_models.clone()),
-            );
-
-            // LM Studio
-            let lmstudio = value.lmstudio.clone();
-
-            merge(
-                &mut settings.lmstudio.api_url,
-                value.lmstudio.as_ref().and_then(|s| s.api_url.clone()),
-            );
-            merge(
-                &mut settings.lmstudio.available_models,
-                lmstudio.as_ref().and_then(|s| s.available_models.clone()),
-            );
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(models) = content.language_models.as_ref() else {
+            return;
+        };
 
-            // DeepSeek
-            let deepseek = value.deepseek.clone();
+        if let Some(anthropic) = models.anthropic.as_ref() {
+            self.anthropic
+                .available_models
+                .merge_from(&anthropic.available_models);
+            self.anthropic.api_url.merge_from(&anthropic.api_url);
+        }
 
-            merge(
-                &mut settings.deepseek.api_url,
-                value.deepseek.as_ref().and_then(|s| s.api_url.clone()),
-            );
-            merge(
-                &mut settings.deepseek.available_models,
-                deepseek.as_ref().and_then(|s| s.available_models.clone()),
-            );
+        if let Some(bedrock) = models.bedrock.clone() {
+            self.bedrock
+                .available_models
+                .merge_from(&bedrock.available_models);
 
-            // OpenAI
-            let openai = value.openai.clone();
-            merge(
-                &mut settings.openai.api_url,
-                openai.as_ref().and_then(|s| s.api_url.clone()),
-            );
-            merge(
-                &mut settings.openai.available_models,
-                openai.as_ref().and_then(|s| s.available_models.clone()),
-            );
+            if let Some(endpoint_url) = bedrock.endpoint_url {
+                self.bedrock.endpoint = Some(endpoint_url)
+            }
 
-            // OpenAI Compatible
-            if let Some(openai_compatible) = value.openai_compatible.clone() {
-                for (id, openai_compatible_settings) in openai_compatible {
-                    settings.openai_compatible.insert(
-                        id,
-                        OpenAiCompatibleSettings {
-                            api_url: openai_compatible_settings.api_url,
-                            available_models: openai_compatible_settings.available_models,
-                        },
-                    );
-                }
+            if let Some(region) = bedrock.region {
+                self.bedrock.region = Some(region)
             }
 
-            // Vercel
-            let vercel = value.vercel.clone();
-            merge(
-                &mut settings.vercel.api_url,
-                vercel.as_ref().and_then(|s| s.api_url.clone()),
-            );
-            merge(
-                &mut settings.vercel.available_models,
-                vercel.as_ref().and_then(|s| s.available_models.clone()),
-            );
+            if let Some(profile_name) = bedrock.profile {
+                self.bedrock.profile_name = Some(profile_name);
+            }
 
-            // XAI
-            let x_ai = value.x_ai.clone();
-            merge(
-                &mut settings.x_ai.api_url,
-                x_ai.as_ref().and_then(|s| s.api_url.clone()),
-            );
-            merge(
-                &mut settings.x_ai.available_models,
-                x_ai.as_ref().and_then(|s| s.available_models.clone()),
-            );
+            if let Some(auth_method) = bedrock.authentication_method {
+                self.bedrock.authentication_method = Some(auth_method.into());
+            }
+        }
 
-            // ZedDotDev
-            merge(
-                &mut settings.zed_dot_dev.available_models,
-                value
-                    .zed_dot_dev
-                    .as_ref()
-                    .and_then(|s| s.available_models.clone()),
-            );
-            merge(
-                &mut settings.google.api_url,
-                value.google.as_ref().and_then(|s| s.api_url.clone()),
-            );
-            merge(
-                &mut settings.google.available_models,
-                value
-                    .google
-                    .as_ref()
-                    .and_then(|s| s.available_models.clone()),
-            );
+        if let Some(deepseek) = models.deepseek.as_ref() {
+            self.deepseek
+                .available_models
+                .merge_from(&deepseek.available_models);
+            self.deepseek.api_url.merge_from(&deepseek.api_url);
+        }
 
-            // Mistral
-            let mistral = value.mistral.clone();
-            merge(
-                &mut settings.mistral.api_url,
-                mistral.as_ref().and_then(|s| s.api_url.clone()),
-            );
-            merge(
-                &mut settings.mistral.available_models,
-                mistral.as_ref().and_then(|s| s.available_models.clone()),
-            );
+        if let Some(google) = models.google.as_ref() {
+            self.google
+                .available_models
+                .merge_from(&google.available_models);
+            self.google.api_url.merge_from(&google.api_url);
+        }
 
-            // OpenRouter
-            let open_router = value.open_router.clone();
-            merge(
-                &mut settings.open_router.api_url,
-                open_router.as_ref().and_then(|s| s.api_url.clone()),
-            );
-            merge(
-                &mut settings.open_router.available_models,
-                open_router
-                    .as_ref()
-                    .and_then(|s| s.available_models.clone()),
-            );
+        if let Some(lmstudio) = models.lmstudio.as_ref() {
+            self.lmstudio
+                .available_models
+                .merge_from(&lmstudio.available_models);
+            self.lmstudio.api_url.merge_from(&lmstudio.api_url);
         }
 
-        Ok(settings)
+        if let Some(mistral) = models.mistral.as_ref() {
+            self.mistral
+                .available_models
+                .merge_from(&mistral.available_models);
+            self.mistral.api_url.merge_from(&mistral.api_url);
+        }
+        if let Some(ollama) = models.ollama.as_ref() {
+            self.ollama
+                .available_models
+                .merge_from(&ollama.available_models);
+            self.ollama.api_url.merge_from(&ollama.api_url);
+        }
+        if let Some(open_router) = models.open_router.as_ref() {
+            self.open_router
+                .available_models
+                .merge_from(&open_router.available_models);
+            self.open_router.api_url.merge_from(&open_router.api_url);
+        }
+        if let Some(openai) = models.openai.as_ref() {
+            self.openai
+                .available_models
+                .merge_from(&openai.available_models);
+            self.openai.api_url.merge_from(&openai.api_url);
+        }
+        if let Some(openai_compatible) = models.openai_compatible.clone() {
+            for (name, value) in openai_compatible {
+                self.openai_compatible.insert(
+                    name,
+                    OpenAiCompatibleSettings {
+                        api_url: value.api_url,
+                        available_models: value.available_models,
+                    },
+                );
+            }
+        }
+        if let Some(vercel) = models.vercel.as_ref() {
+            self.vercel
+                .available_models
+                .merge_from(&vercel.available_models);
+            self.vercel.api_url.merge_from(&vercel.api_url);
+        }
+        if let Some(x_ai) = models.x_ai.as_ref() {
+            self.x_ai
+                .available_models
+                .merge_from(&x_ai.available_models);
+            self.x_ai.api_url.merge_from(&x_ai.api_url);
+        }
+        if let Some(zed_dot_dev) = models.zed_dot_dev.as_ref() {
+            self.zed_dot_dev
+                .available_models
+                .merge_from(&zed_dot_dev.available_models);
+        }
     }
-
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
 }

crates/languages/src/bash.rs πŸ”—

@@ -19,7 +19,7 @@ pub(super) fn bash_task_context() -> ContextProviderWithTasks {
 #[cfg(test)]
 mod tests {
     use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
-    use language::{AutoindentMode, Buffer, language_settings::AllLanguageSettings};
+    use language::{AutoindentMode, Buffer};
     use settings::SettingsStore;
     use std::num::NonZeroU32;
     use unindent::Unindent;
@@ -34,8 +34,8 @@ mod tests {
             cx.set_global(test_settings);
             language::init(cx);
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
-                    s.defaults.tab_size = NonZeroU32::new(2)
+                store.update_user_settings(cx, |s| {
+                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2)
                 });
             });
         });

crates/languages/src/c.rs πŸ”—

@@ -394,7 +394,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
 #[cfg(test)]
 mod tests {
     use gpui::{AppContext as _, BorrowAppContext, TestAppContext};
-    use language::{AutoindentMode, Buffer, language_settings::AllLanguageSettings};
+    use language::{AutoindentMode, Buffer};
     use settings::SettingsStore;
     use std::num::NonZeroU32;
 
@@ -406,8 +406,8 @@ mod tests {
             cx.set_global(test_settings);
             language::init(cx);
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
-                    s.defaults.tab_size = NonZeroU32::new(2);
+                store.update_user_settings(cx, |s| {
+                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
                 });
             });
         });

crates/languages/src/lib.rs πŸ”—

@@ -312,7 +312,12 @@ pub fn init(languages: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, node: NodeRuntime
                 cx.update(|cx| {
                     SettingsStore::update_global(cx, |settings, cx| {
                         settings
-                            .set_extension_settings(language_settings.clone(), cx)
+                            .set_extension_settings(
+                                settings::ExtensionsSettingsContent {
+                                    all_languages: language_settings.clone(),
+                                },
+                                cx,
+                            )
                             .log_err();
                     });
                 })?;

crates/languages/src/python.rs πŸ”—

@@ -2169,7 +2169,7 @@ impl LspInstaller for RuffLspAdapter {
 #[cfg(test)]
 mod tests {
     use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
-    use language::{AutoindentMode, Buffer, language_settings::AllLanguageSettings};
+    use language::{AutoindentMode, Buffer};
     use settings::SettingsStore;
     use std::num::NonZeroU32;
 
@@ -2182,8 +2182,8 @@ mod tests {
             cx.set_global(test_settings);
             language::init(cx);
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
-                    s.defaults.tab_size = NonZeroU32::new(2);
+                store.update_user_settings(cx, |s| {
+                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
                 });
             });
         });

crates/languages/src/rust.rs πŸ”—

@@ -1051,7 +1051,6 @@ mod tests {
     use super::*;
     use crate::language;
     use gpui::{BorrowAppContext, Hsla, TestAppContext};
-    use language::language_settings::AllLanguageSettings;
     use lsp::CompletionItemLabelDetails;
     use settings::SettingsStore;
     use theme::SyntaxTheme;
@@ -1381,8 +1380,8 @@ mod tests {
             cx.set_global(test_settings);
             language::init(cx);
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
-                    s.defaults.tab_size = NonZeroU32::new(2);
+                store.update_user_settings(cx, |s| {
+                    s.project.all_languages.defaults.tab_size = NonZeroU32::new(2);
                 });
             });
         });

crates/markdown/examples/markdown.rs πŸ”—

@@ -1,6 +1,6 @@
 use assets::Assets;
 use gpui::{Application, Entity, KeyBinding, StyleRefinement, WindowOptions, prelude::*, rgb};
-use language::{LanguageRegistry, language_settings::AllLanguageSettings};
+use language::LanguageRegistry;
 use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 use node_runtime::NodeRuntime;
 use settings::SettingsStore;
@@ -39,9 +39,6 @@ pub fn main() {
         let store = SettingsStore::test(cx);
         cx.set_global(store);
         language::init(cx);
-        SettingsStore::update(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
-        });
         cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]);
 
         let node_runtime = NodeRuntime::unavailable();

crates/markdown/examples/markdown_as_child.rs πŸ”—

@@ -1,6 +1,6 @@
 use assets::Assets;
 use gpui::{Application, Entity, KeyBinding, Length, StyleRefinement, WindowOptions, rgb};
-use language::{LanguageRegistry, language_settings::AllLanguageSettings};
+use language::LanguageRegistry;
 use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 use node_runtime::NodeRuntime;
 use settings::SettingsStore;
@@ -23,9 +23,6 @@ pub fn main() {
         let store = SettingsStore::test(cx);
         cx.set_global(store);
         language::init(cx);
-        SettingsStore::update(cx, |store, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
-        });
         cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]);
 
         let node_runtime = NodeRuntime::unavailable();

crates/multi_buffer/src/multi_buffer.rs πŸ”—

@@ -17,11 +17,11 @@ use gpui::{App, AppContext as _, Context, Entity, EntityId, EventEmitter, Task};
 use itertools::Itertools;
 use language::{
     AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier,
-    CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentSize, Language,
-    LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection,
-    TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions,
-    Unclipped,
-    language_settings::{IndentGuideSettings, LanguageSettings, language_settings},
+    CharKind, Chunk, CursorShape, DiagnosticEntry, DiskState, File, IndentGuideSettings,
+    IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point,
+    PointUtf16, Selection, TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId,
+    TreeSitterOptions, Unclipped,
+    language_settings::{LanguageSettings, language_settings},
 };
 
 use rope::DimensionPair;
@@ -5913,7 +5913,7 @@ impl MultiBufferSnapshot {
                             end_row: last_row,
                             depth: next_depth,
                             tab_size,
-                            settings: settings.indent_guides,
+                            settings: settings.indent_guides.clone(),
                         });
                     }
                 }

crates/ollama/Cargo.toml πŸ”—

@@ -22,4 +22,5 @@ http_client.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
+settings.workspace = true
 workspace-hack.workspace = true

crates/ollama/src/ollama.rs πŸ”—

@@ -3,33 +3,11 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B
 use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http};
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
+pub use settings::KeepAlive;
 use std::time::Duration;
 
 pub const OLLAMA_API_URL: &str = "http://localhost:11434";
 
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
-#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
-#[serde(untagged)]
-pub enum KeepAlive {
-    /// Keep model alive for N seconds
-    Seconds(isize),
-    /// Keep model alive for a fixed duration. Accepts durations like "5m", "10m", "1h", "1d", etc.
-    Duration(String),
-}
-
-impl KeepAlive {
-    /// Keep model alive until a new model is loaded or until Ollama shuts down
-    fn indefinite() -> Self {
-        Self::Seconds(-1)
-    }
-}
-
-impl Default for KeepAlive {
-    fn default() -> Self {
-        Self::indefinite()
-    }
-}
-
 #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
 pub struct Model {

crates/onboarding/Cargo.toml πŸ”—

@@ -45,3 +45,6 @@ workspace-hack.workspace = true
 workspace.workspace = true
 zed_actions.workspace = true
 zlog.workspace = true
+
+[dev-dependencies]
+db = {workspace = true, features = ["test-support"]}

crates/onboarding/src/ai_setup_page.rs πŸ”—

@@ -264,8 +264,8 @@ pub(crate) fn render_ai_setup_page(
                     );
 
                     let fs = <dyn Fs>::global(cx);
-                    update_settings_file::<DisableAiSettings>(fs, cx, move |ai_settings, _| {
-                        ai_settings.disable_ai = Some(enabled);
+                    update_settings_file(fs, cx, move |settings, _| {
+                        settings.disable_ai = Some(enabled);
                     });
                 },
             )

crates/onboarding/src/base_keymap_picker.rs πŸ”—

@@ -186,8 +186,8 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
                 value = base_keymap.to_string()
             );
 
-            update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting, _| {
-                setting.base_keymap = Some(base_keymap)
+            update_settings_file(self.fs.clone(), cx, move |setting, _| {
+                setting.base_keymap = Some(base_keymap.into())
             });
         }
 

crates/onboarding/src/basics_page.rs πŸ”—

@@ -194,27 +194,27 @@ fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement
 
     fn write_mode_change(mode: ThemeMode, cx: &mut App) {
         let fs = <dyn Fs>::global(cx);
-        update_settings_file::<ThemeSettings>(fs, cx, move |settings, _cx| {
-            settings.set_mode(mode);
+        update_settings_file(fs, cx, move |settings, _cx| {
+            theme::set_mode(settings, mode);
         });
     }
 
     fn write_theme_change(theme: impl Into<Arc<str>>, theme_mode: ThemeMode, cx: &mut App) {
         let fs = <dyn Fs>::global(cx);
         let theme = theme.into();
-        update_settings_file::<ThemeSettings>(fs, cx, move |settings, cx| {
+        update_settings_file(fs, cx, move |settings, cx| {
             if theme_mode == ThemeMode::System {
                 let (light_theme, dark_theme) =
                     get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref()));
 
-                settings.theme = Some(ThemeSelection::Dynamic {
+                settings.theme.theme = Some(settings::ThemeSelection::Dynamic {
                     mode: ThemeMode::System,
                     light: ThemeName(light_theme.into()),
                     dark: ThemeName(dark_theme.into()),
                 });
             } else {
                 let appearance = *SystemAppearance::global(cx);
-                settings.set_theme(theme, appearance);
+                theme::set_theme(settings, theme, appearance);
             }
         });
     }
@@ -247,10 +247,13 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
                     ToggleState::Indeterminate => { return; },
                 };
 
-                update_settings_file::<TelemetrySettings>(
+                update_settings_file(
                     fs.clone(),
                     cx,
-                    move |setting, _| setting.metrics = Some(enabled),
+                    move |setting, _| {
+                        setting.telemetry.get_or_insert_default().metrics = Some(enabled);
+                    }
+                    ,
                 );
 
                 // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
@@ -286,10 +289,13 @@ fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement
                         ToggleState::Indeterminate => { return; },
                     };
 
-                    update_settings_file::<TelemetrySettings>(
+                    update_settings_file(
                         fs.clone(),
                         cx,
-                        move |setting, _| setting.diagnostics = Some(enabled),
+                        move |setting, _| {
+                            setting.telemetry.get_or_insert_default().diagnostics = Some(enabled);
+                        },
+
                     );
 
                     // This telemetry event shouldn't fire when it's off. If it does we'll be alerted
@@ -358,8 +364,8 @@ fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoE
     fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
         let fs = <dyn Fs>::global(cx);
 
-        update_settings_file::<BaseKeymap>(fs, cx, move |setting, _| {
-            setting.base_keymap = Some(keymap_base);
+        update_settings_file(fs, cx, move |setting, _| {
+            setting.base_keymap = Some(keymap_base.into());
         });
 
         telemetry::event!("Welcome Keymap Changed", keymap = keymap_base);
@@ -387,7 +393,7 @@ fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoEleme
                         return;
                     }
                 };
-                update_settings_file::<VimModeSetting>(fs.clone(), cx, move |setting, _| {
+                update_settings_file(fs.clone(), cx, move |setting, _| {
                     setting.vim_mode = Some(vim_mode);
                 });
 

crates/onboarding/src/editing_page.rs πŸ”—

@@ -34,13 +34,13 @@ fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
     curr_settings.minimap.show = show;
     EditorSettings::override_global(curr_settings, cx);
 
-    update_settings_file::<EditorSettings>(fs, cx, move |editor_settings, _| {
+    update_settings_file(fs, cx, move |settings, _| {
         telemetry::event!(
             "Welcome Minimap Clicked",
-            from = editor_settings.minimap.unwrap_or_default(),
+            from = settings.editor.minimap.clone().unwrap_or_default(),
             to = show
         );
-        editor_settings.minimap.get_or_insert_default().show = Some(show);
+        settings.editor.minimap.get_or_insert_default().show = Some(show);
     });
 }
 
@@ -58,86 +58,80 @@ fn write_inlay_hints(enabled: bool, cx: &mut App) {
     curr_settings.defaults.inlay_hints.enabled = enabled;
     AllLanguageSettings::override_global(curr_settings, cx);
 
-    update_settings_file::<AllLanguageSettings>(fs, cx, move |all_language_settings, cx| {
-        all_language_settings
+    update_settings_file(fs, cx, move |settings, _cx| {
+        settings
+            .project
+            .all_languages
             .defaults
             .inlay_hints
-            .get_or_insert_with(|| {
-                AllLanguageSettings::get_global(cx)
-                    .clone()
-                    .defaults
-                    .inlay_hints
-            })
-            .enabled = enabled;
+            .get_or_insert_default()
+            .enabled = Some(enabled);
     });
 }
 
 fn read_git_blame(cx: &App) -> bool {
-    ProjectSettings::get_global(cx).git.inline_blame_enabled()
+    ProjectSettings::get_global(cx).git.inline_blame.enabled
 }
 
 fn write_git_blame(enabled: bool, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
     let mut curr_settings = ProjectSettings::get_global(cx).clone();
-    curr_settings
-        .git
-        .inline_blame
-        .get_or_insert_default()
-        .enabled = enabled;
+    curr_settings.git.inline_blame.enabled = enabled;
     ProjectSettings::override_global(curr_settings, cx);
 
-    update_settings_file::<ProjectSettings>(fs, cx, move |project_settings, _| {
-        project_settings
+    update_settings_file(fs, cx, move |settings, _| {
+        settings
             .git
+            .get_or_insert_default()
             .inline_blame
             .get_or_insert_default()
-            .enabled = enabled;
+            .enabled = Some(enabled);
     });
 }
 
 fn write_ui_font_family(font: SharedString, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
-    update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
+    update_settings_file(fs, cx, move |settings, _| {
         telemetry::event!(
             "Welcome Font Changed",
             type = "ui font",
-            old = theme_settings.ui_font_family,
+            old = settings.theme.ui_font_family,
             new = font
         );
-        theme_settings.ui_font_family = Some(FontFamilyName(font.into()));
+        settings.theme.ui_font_family = Some(FontFamilyName(font.into()));
     });
 }
 
 fn write_ui_font_size(size: Pixels, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
-    update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
-        theme_settings.ui_font_size = Some(size.into());
+    update_settings_file(fs, cx, move |settings, _| {
+        settings.theme.ui_font_size = Some(size.into());
     });
 }
 
 fn write_buffer_font_size(size: Pixels, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
-    update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
-        theme_settings.buffer_font_size = Some(size.into());
+    update_settings_file(fs, cx, move |settings, _| {
+        settings.theme.buffer_font_size = Some(size.into());
     });
 }
 
 fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
-    update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
+    update_settings_file(fs, cx, move |settings, _| {
         telemetry::event!(
             "Welcome Font Changed",
             type = "editor font",
-            old = theme_settings.buffer_font_family,
+            old = settings.theme.buffer_font_family,
             new = font_family
         );
 
-        theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into()));
+        settings.theme.buffer_font_family = Some(FontFamilyName(font_family.into()));
     });
 }
 
@@ -153,8 +147,9 @@ fn write_font_ligatures(enabled: bool, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
     let bit = if enabled { 1 } else { 0 };
 
-    update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
-        let mut features = theme_settings
+    update_settings_file(fs, cx, move |settings, _| {
+        let mut features = settings
+            .theme
             .buffer_font_features
             .as_mut()
             .map(|features| features.tag_value_list().to_vec())
@@ -166,7 +161,7 @@ fn write_font_ligatures(enabled: bool, cx: &mut App) {
             features.push(("calt".into(), bit));
         }
 
-        theme_settings.buffer_font_features = Some(FontFeatures(Arc::new(features)));
+        settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features)));
     });
 }
 
@@ -180,8 +175,8 @@ fn read_format_on_save(cx: &App) -> bool {
 fn write_format_on_save(format_on_save: bool, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
 
-    update_settings_file::<AllLanguageSettings>(fs, cx, move |language_settings, _| {
-        language_settings.defaults.format_on_save = Some(match format_on_save {
+    update_settings_file(fs, cx, move |settings, _| {
+        settings.project.all_languages.defaults.format_on_save = Some(match format_on_save {
             true => FormatOnSave::On,
             false => FormatOnSave::Off,
         });

crates/open_ai/Cargo.toml πŸ”—

@@ -23,5 +23,6 @@ schemars = { workspace = true, optional = true }
 log.workspace = true
 serde.workspace = true
 serde_json.workspace = true
+settings.workspace = true
 strum.workspace = true
 workspace-hack.workspace = true

crates/open_ai/src/open_ai.rs πŸ”—

@@ -3,6 +3,7 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B
 use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
+pub use settings::OpenAiReasoningEffort as ReasoningEffort;
 use std::{convert::TryFrom, future::Future};
 use strum::EnumIter;
 
@@ -278,16 +279,6 @@ pub enum ToolChoice {
     Other(ToolDefinition),
 }
 
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
-#[serde(rename_all = "lowercase")]
-pub enum ReasoningEffort {
-    Minimal,
-    Low,
-    Medium,
-    High,
-}
-
 #[derive(Clone, Deserialize, Serialize, Debug)]
 #[serde(tag = "type", rename_all = "snake_case")]
 pub enum ToolDefinition {

crates/open_router/Cargo.toml πŸ”—

@@ -22,7 +22,7 @@ http_client.workspace = true
 schemars = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true
-thiserror.workspace = true
+settings.workspace = true
 strum.workspace = true
-util.workspace = true
+thiserror.workspace = true
 workspace-hack.workspace = true

crates/open_router/src/open_router.rs πŸ”—

@@ -3,10 +3,13 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B
 use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http};
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
+pub use settings::DataCollection;
+pub use settings::ModelMode;
+pub use settings::OpenRouterAvailableModel as AvailableModel;
+pub use settings::OpenRouterProvider as Provider;
 use std::{convert::TryFrom, io, time::Duration};
 use strum::EnumString;
 use thiserror::Error;
-use util::serde::default_true;
 
 pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1";
 
@@ -65,41 +68,6 @@ impl From<Role> for String {
     }
 }
 
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
-pub enum DataCollection {
-    Allow,
-    Disallow,
-}
-
-impl Default for DataCollection {
-    fn default() -> Self {
-        Self::Allow
-    }
-}
-
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
-pub struct Provider {
-    #[serde(skip_serializing_if = "Option::is_none")]
-    order: Option<Vec<String>>,
-    #[serde(default = "default_true")]
-    allow_fallbacks: bool,
-    #[serde(default)]
-    require_parameters: bool,
-    #[serde(default)]
-    data_collection: DataCollection,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    only: Option<Vec<String>>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    ignore: Option<Vec<String>>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    quantizations: Option<Vec<String>>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    sort: Option<String>,
-}
-
 #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
 pub struct Model {
@@ -113,16 +81,6 @@ pub struct Model {
     pub provider: Option<Provider>,
 }
 
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
-pub enum ModelMode {
-    #[default]
-    Default,
-    Thinking {
-        budget_tokens: Option<u32>,
-    },
-}
-
 impl Model {
     pub fn default_fast() -> Self {
         Self::new(

crates/outline_panel/Cargo.toml πŸ”—

@@ -26,7 +26,6 @@ log.workspace = true
 menu.workspace = true
 outline.workspace = true
 project.workspace = true
-schemars.workspace = true
 search.workspace = true
 serde.workspace = true
 serde_json.workspace = true

crates/outline_panel/src/outline_panel.rs πŸ”—

@@ -38,7 +38,7 @@ use std::{
     u32,
 };
 
-use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings, ShowIndentGuides};
+use outline_panel_settings::{DockSide, OutlinePanelSettings, ShowIndentGuides};
 use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem};
 use search::{BufferSearchBar, ProjectSearchView};
 use serde::{Deserialize, Serialize};
@@ -4840,8 +4840,8 @@ impl Panel for OutlinePanel {
 
     fn position(&self, _: &Window, cx: &App) -> DockPosition {
         match OutlinePanelSettings::get_global(cx).dock {
-            OutlinePanelDockPosition::Left => DockPosition::Left,
-            OutlinePanelDockPosition::Right => DockPosition::Right,
+            DockSide::Left => DockPosition::Left,
+            DockSide::Right => DockPosition::Right,
         }
     }
 
@@ -4850,17 +4850,13 @@ impl Panel for OutlinePanel {
     }
 
     fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
-        settings::update_settings_file::<OutlinePanelSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings, _| {
-                let dock = match position {
-                    DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
-                    DockPosition::Right => OutlinePanelDockPosition::Right,
-                };
-                settings.dock = Some(dock);
-            },
-        );
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            let dock = match position {
+                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
+                DockPosition::Right => DockSide::Right,
+            };
+            settings.outline_panel.get_or_insert_default().dock = Some(dock);
+        });
     }
 
     fn size(&self, _: &Window, cx: &App) -> Pixels {

crates/outline_panel/src/outline_panel_settings.rs πŸ”—

@@ -1,29 +1,14 @@
 use editor::EditorSettings;
 use gpui::{App, Pixels};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+pub use settings::{DockSide, Settings, ShowIndentGuides};
 use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
+use util::MergeFrom;
 
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
-#[serde(rename_all = "snake_case")]
-pub enum OutlinePanelDockPosition {
-    Left,
-    Right,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowIndentGuides {
-    Always,
-    Never,
-}
-
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq)]
 pub struct OutlinePanelSettings {
     pub button: bool,
     pub default_width: Pixels,
-    pub dock: OutlinePanelDockPosition,
+    pub dock: DockSide,
     pub file_icons: bool,
     pub folder_icons: bool,
     pub git_status: bool,
@@ -35,7 +20,7 @@ pub struct OutlinePanelSettings {
     pub expand_outlines_with_depth: usize,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub struct ScrollbarSettings {
     /// When to show the scrollbar in the project panel.
     ///
@@ -43,80 +28,11 @@ pub struct ScrollbarSettings {
     pub show: Option<ShowScrollbar>,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-pub struct ScrollbarSettingsContent {
-    /// When to show the scrollbar in the project panel.
-    ///
-    /// Default: inherits editor scrollbar settings
-    pub show: Option<Option<ShowScrollbar>>,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub struct IndentGuidesSettings {
     pub show: ShowIndentGuides,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-pub struct IndentGuidesSettingsContent {
-    /// When to show the scrollbar in the outline panel.
-    pub show: Option<ShowIndentGuides>,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "outline_panel")]
-pub struct OutlinePanelSettingsContent {
-    /// Whether to show the outline panel button in the status bar.
-    ///
-    /// Default: true
-    pub button: Option<bool>,
-    /// Customize default width (in pixels) taken by outline panel
-    ///
-    /// Default: 240
-    pub default_width: Option<f32>,
-    /// The position of outline panel
-    ///
-    /// Default: left
-    pub dock: Option<OutlinePanelDockPosition>,
-    /// Whether to show file icons in the outline panel.
-    ///
-    /// Default: true
-    pub file_icons: Option<bool>,
-    /// Whether to show folder icons or chevrons for directories in the outline panel.
-    ///
-    /// Default: true
-    pub folder_icons: Option<bool>,
-    /// Whether to show the git status in the outline panel.
-    ///
-    /// Default: true
-    pub git_status: Option<bool>,
-    /// Amount of indentation (in pixels) for nested items.
-    ///
-    /// Default: 20
-    pub indent_size: Option<f32>,
-    /// Whether to reveal it in the outline panel automatically,
-    /// when a corresponding project entry becomes active.
-    /// Gitignored entries are never auto revealed.
-    ///
-    /// Default: true
-    pub auto_reveal_entries: Option<bool>,
-    /// Whether to fold directories automatically
-    /// when directory has only one directory inside.
-    ///
-    /// Default: true
-    pub auto_fold_dirs: Option<bool>,
-    /// Settings related to indent guides in the outline panel.
-    pub indent_guides: Option<IndentGuidesSettingsContent>,
-    /// Scrollbar-related settings
-    pub scrollbar: Option<ScrollbarSettingsContent>,
-    /// Default depth to expand outline items in the current file.
-    /// The default depth to which outline entries are expanded on reveal.
-    /// - Set to 0 to collapse all items that have children
-    /// - Set to 1 or higher to collapse items at that depth or deeper
-    ///
-    /// Default: 100
-    pub expand_outlines_with_depth: Option<usize>,
-}
-
 impl ScrollbarVisibility for OutlinePanelSettings {
     fn visibility(&self, cx: &App) -> ShowScrollbar {
         self.scrollbar
@@ -126,21 +42,69 @@ impl ScrollbarVisibility for OutlinePanelSettings {
 }
 
 impl Settings for OutlinePanelSettings {
-    type FileContent = OutlinePanelSettingsContent;
-
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _: &mut gpui::App,
-    ) -> anyhow::Result<Self> {
-        sources.json_merge()
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let panel = content.outline_panel.as_ref().unwrap();
+        Self {
+            button: panel.button.unwrap(),
+            default_width: panel.default_width.map(gpui::px).unwrap(),
+            dock: panel.dock.unwrap(),
+            file_icons: panel.file_icons.unwrap(),
+            folder_icons: panel.folder_icons.unwrap(),
+            git_status: panel.git_status.unwrap(),
+            indent_size: panel.indent_size.unwrap(),
+            indent_guides: IndentGuidesSettings {
+                show: panel.indent_guides.unwrap().show.unwrap(),
+            },
+            auto_reveal_entries: panel.auto_reveal_entries.unwrap(),
+            auto_fold_dirs: panel.auto_fold_dirs.unwrap(),
+            scrollbar: ScrollbarSettings {
+                show: panel.scrollbar.unwrap().show.flatten().map(Into::into),
+            },
+            expand_outlines_with_depth: panel.expand_outlines_with_depth.unwrap(),
+        }
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(panel) = content.outline_panel.as_ref() else {
+            return;
+        };
+
+        self.button.merge_from(&panel.button);
+        self.default_width
+            .merge_from(&panel.default_width.map(Pixels::from));
+        self.dock.merge_from(&panel.dock);
+        self.file_icons.merge_from(&panel.file_icons);
+        self.folder_icons.merge_from(&panel.folder_icons);
+        self.git_status.merge_from(&panel.git_status);
+        self.indent_size.merge_from(&panel.indent_size);
+
+        if let Some(indent_guides) = panel.indent_guides.as_ref() {
+            self.indent_guides.show.merge_from(&indent_guides.show);
+        }
+
+        self.auto_reveal_entries
+            .merge_from(&panel.auto_reveal_entries);
+        self.auto_fold_dirs.merge_from(&panel.auto_fold_dirs);
+
+        if let Some(scrollbar) = panel.scrollbar.as_ref()
+            && let Some(show) = scrollbar.show.flatten()
+        {
+            self.scrollbar.show = Some(show.into())
+        }
+    }
+    fn import_from_vscode(
+        vscode: &settings::VsCodeSettings,
+        current: &mut settings::SettingsContent,
+    ) {
         if let Some(b) = vscode.read_bool("outline.icons") {
-            current.file_icons = Some(b);
-            current.folder_icons = Some(b);
+            let outline_panel = current.outline_panel.get_or_insert_default();
+            outline_panel.file_icons = Some(b);
+            outline_panel.folder_icons = Some(b);
         }
 
-        vscode.bool_setting("git.decorations.enabled", &mut current.git_status);
+        if let Some(b) = vscode.read_bool("git.decorations.enabled") {
+            let outline_panel = current.outline_panel.get_or_insert_default();
+            outline_panel.git_status = Some(b);
+        }
     }
 }

crates/project/src/agent_server_store.rs πŸ”—

@@ -22,7 +22,7 @@ use rpc::{
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{SettingsKey, SettingsSources, SettingsStore, SettingsUi};
+use settings::{SettingsContent, SettingsKey, SettingsStore, SettingsUi};
 use util::{ResultExt as _, debug_panic};
 
 use crate::ProjectEnvironment;
@@ -989,47 +989,19 @@ impl ExternalAgentServer for LocalCustomAgent {
 pub const GEMINI_NAME: &'static str = "gemini";
 pub const CLAUDE_CODE_NAME: &'static str = "claude";
 
-#[derive(
-    Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey, PartialEq,
-)]
+#[derive(Default, Clone, JsonSchema, Debug, SettingsUi, SettingsKey, PartialEq)]
 #[settings_key(key = "agent_servers")]
 pub struct AllAgentServersSettings {
     pub gemini: Option<BuiltinAgentServerSettings>,
     pub claude: Option<BuiltinAgentServerSettings>,
-
-    /// Custom agent servers configured by the user
-    #[serde(flatten)]
     pub custom: HashMap<SharedString, CustomAgentServerSettings>,
 }
-
-#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+#[derive(Default, Clone, JsonSchema, Debug, PartialEq)]
 pub struct BuiltinAgentServerSettings {
-    /// Absolute path to a binary to be used when launching this agent.
-    ///
-    /// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
-    #[serde(rename = "command")]
     pub path: Option<PathBuf>,
-    /// If a binary is specified in `command`, it will be passed these arguments.
     pub args: Option<Vec<String>>,
-    /// If a binary is specified in `command`, it will be passed these environment variables.
     pub env: Option<HashMap<String, String>>,
-    /// Whether to skip searching `$PATH` for an agent server binary when
-    /// launching this agent.
-    ///
-    /// This has no effect if a `command` is specified. Otherwise, when this is
-    /// `false`, Zed will search `$PATH` for an agent server binary and, if one
-    /// is found, use it for threads with this agent. If no agent binary is
-    /// found on `$PATH`, Zed will automatically install and use its own binary.
-    /// When this is `true`, Zed will not search `$PATH`, and will always use
-    /// its own binary.
-    ///
-    /// Default: true
     pub ignore_system_version: Option<bool>,
-    /// The default mode to use for this agent.
-    ///
-    /// Note: Not only all agents support modes.
-    ///
-    /// Default: None
     pub default_mode: Option<String>,
 }
 
@@ -1043,6 +1015,18 @@ impl BuiltinAgentServerSettings {
     }
 }
 
+impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
+    fn from(value: settings::BuiltinAgentServerSettings) -> Self {
+        BuiltinAgentServerSettings {
+            path: value.path,
+            args: value.args,
+            env: value.env,
+            ignore_system_version: value.ignore_system_version,
+            default_mode: value.default_mode,
+        }
+    }
+}
+
 impl From<AgentServerCommand> for BuiltinAgentServerSettings {
     fn from(value: AgentServerCommand) -> Self {
         BuiltinAgentServerSettings {
@@ -1054,9 +1038,8 @@ impl From<AgentServerCommand> for BuiltinAgentServerSettings {
     }
 }
 
-#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+#[derive(Clone, JsonSchema, Debug, PartialEq)]
 pub struct CustomAgentServerSettings {
-    #[serde(flatten)]
     pub command: AgentServerCommand,
     /// The default mode to use for this agent.
     ///
@@ -1066,36 +1049,47 @@ pub struct CustomAgentServerSettings {
     pub default_mode: Option<String>,
 }
 
-impl settings::Settings for AllAgentServersSettings {
-    type FileContent = Self;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        let mut settings = AllAgentServersSettings::default();
-
-        for AllAgentServersSettings {
-            gemini,
-            claude,
-            custom,
-        } in sources.defaults_and_customizations()
-        {
-            if gemini.is_some() {
-                settings.gemini = gemini.clone();
-            }
-            if claude.is_some() {
-                settings.claude = claude.clone();
-            }
+impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
+    fn from(value: settings::CustomAgentServerSettings) -> Self {
+        CustomAgentServerSettings {
+            command: AgentServerCommand {
+                path: value.path,
+                args: value.args,
+                env: value.env,
+            },
+            default_mode: value.default_mode,
+        }
+    }
+}
 
-            // Merge custom agents
-            for (name, config) in custom {
-                // Skip built-in agent names to avoid conflicts
-                if name != GEMINI_NAME && name != CLAUDE_CODE_NAME {
-                    settings.custom.insert(name.clone(), config.clone());
-                }
-            }
+impl settings::Settings for AllAgentServersSettings {
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let agent_settings = content.agent_servers.clone().unwrap();
+        Self {
+            gemini: agent_settings.gemini.map(Into::into),
+            claude: agent_settings.claude.map(Into::into),
+            custom: agent_settings
+                .custom
+                .into_iter()
+                .map(|(k, v)| (k, v.into()))
+                .collect(),
         }
+    }
 
-        Ok(settings)
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(content) = &content.agent_servers else {
+            return;
+        };
+        if let Some(gemini) = content.gemini.clone() {
+            self.gemini = Some(gemini.into())
+        };
+        if let Some(claude) = content.claude.clone() {
+            self.claude = Some(claude.into());
+        }
+        for (name, config) in content.custom.clone() {
+            self.custom.insert(name, config.into());
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
 }

crates/project/src/context_server_store.rs πŸ”—

@@ -924,7 +924,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Extension {
+                    settings::ContextServerSettingsContent::Extension {
                         enabled: true,
                         settings: json!({
                             "somevalue": false
@@ -943,7 +943,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Extension {
+                    settings::ContextServerSettingsContent::Extension {
                         enabled: true,
                         settings: json!({
                             "somevalue": false
@@ -970,7 +970,7 @@ mod tests {
                 vec![
                     (
                         server_1_id.0.clone(),
-                        ContextServerSettings::Extension {
+                        settings::ContextServerSettingsContent::Extension {
                             enabled: true,
                             settings: json!({
                                 "somevalue": false
@@ -979,7 +979,7 @@ mod tests {
                     ),
                     (
                         server_2_id.0.clone(),
-                        ContextServerSettings::Custom {
+                        settings::ContextServerSettingsContent::Custom {
                             enabled: true,
                             command: ContextServerCommand {
                                 path: "somebinary".into(),
@@ -1011,7 +1011,7 @@ mod tests {
                 vec![
                     (
                         server_1_id.0.clone(),
-                        ContextServerSettings::Extension {
+                        settings::ContextServerSettingsContent::Extension {
                             enabled: true,
                             settings: json!({
                                 "somevalue": false
@@ -1020,7 +1020,7 @@ mod tests {
                     ),
                     (
                         server_2_id.0.clone(),
-                        ContextServerSettings::Custom {
+                        settings::ContextServerSettingsContent::Custom {
                             enabled: true,
                             command: ContextServerCommand {
                                 path: "somebinary".into(),
@@ -1047,7 +1047,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Extension {
+                    settings::ContextServerSettingsContent::Extension {
                         enabled: true,
                         settings: json!({
                             "somevalue": false
@@ -1070,7 +1070,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Extension {
+                    settings::ContextServerSettingsContent::Extension {
                         enabled: true,
                         settings: json!({
                             "somevalue": false
@@ -1156,7 +1156,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Custom {
+                    settings::ContextServerSettingsContent::Custom {
                         enabled: false,
                         command: ContextServerCommand {
                             path: "somebinary".into(),
@@ -1185,7 +1185,7 @@ mod tests {
             set_context_server_configuration(
                 vec![(
                     server_1_id.0.clone(),
-                    ContextServerSettings::Custom {
+                    settings::ContextServerSettingsContent::Custom {
                         enabled: true,
                         command: ContextServerCommand {
                             path: "somebinary".into(),
@@ -1203,18 +1203,17 @@ mod tests {
     }
 
     fn set_context_server_configuration(
-        context_servers: Vec<(Arc<str>, ContextServerSettings)>,
+        context_servers: Vec<(Arc<str>, settings::ContextServerSettingsContent)>,
         cx: &mut TestAppContext,
     ) {
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                let mut settings = ProjectSettings::default();
-                for (id, config) in context_servers {
-                    settings.context_servers.insert(id, config);
-                }
-                store
-                    .set_user_settings(&serde_json::to_string(&settings).unwrap(), cx)
-                    .unwrap();
+                store.update_user_settings(cx, |content| {
+                    content.project.context_servers.clear();
+                    for (id, config) in context_servers {
+                        content.project.context_servers.insert(id, config);
+                    }
+                });
             })
         });
     }

crates/project/src/debugger/dap_store.rs πŸ”—

@@ -5,8 +5,10 @@ use super::{
     session::{self, Session, SessionStateEvent},
 };
 use crate::{
-    InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, debugger::session::SessionQuirks,
-    project_settings::ProjectSettings, worktree_store::WorktreeStore,
+    InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
+    debugger::session::SessionQuirks,
+    project_settings::{DapBinary, ProjectSettings},
+    worktree_store::WorktreeStore,
 };
 use anyhow::{Context as _, Result, anyhow};
 use async_trait::async_trait;
@@ -28,8 +30,9 @@ use futures::{
 };
 use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
 use http_client::HttpClient;
-use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind};
+use language::{Buffer, LanguageToolchainStore};
 use node_runtime::NodeRuntime;
+use settings::InlayHintKind;
 
 use remote::RemoteClient;
 use rpc::{
@@ -208,8 +211,10 @@ impl DapStore {
                 let dap_settings = ProjectSettings::get(Some(settings_location), cx)
                     .dap
                     .get(&adapter.name());
-                let user_installed_path =
-                    dap_settings.and_then(|s| s.binary.as_ref().map(PathBuf::from));
+                let user_installed_path = dap_settings.and_then(|s| match &s.binary {
+                    DapBinary::Default => None,
+                    DapBinary::Custom(binary) => Some(PathBuf::from(binary)),
+                });
                 let user_args = dap_settings.map(|s| s.args.clone());
 
                 let delegate = self.delegate(worktree, console, cx);

crates/project/src/lsp_store.rs πŸ”—

@@ -5816,7 +5816,8 @@ impl LspStore {
                 buffer.read(cx).file(),
                 cx,
             )
-            .completions;
+            .completions
+            .clone();
             if !completion_settings.lsp {
                 return Task::ready(Ok(Vec::new()));
             }

crates/project/src/project.rs πŸ”—

@@ -29,7 +29,6 @@ use context_server_store::ContextServerStore;
 pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
 use git::repository::get_git_committer;
 use git_store::{Repository, RepositoryId};
-use schemars::JsonSchema;
 pub mod search_history;
 mod yarn;
 
@@ -101,10 +100,7 @@ use rpc::{
 };
 use search::{SearchInputKind, SearchQuery, SearchResult};
 use search_history::SearchHistory;
-use settings::{
-    InvalidSettingsError, Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsStore,
-    SettingsUi,
-};
+use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore};
 use smol::channel::Receiver;
 use snippet::Snippet;
 use snippet_provider::SnippetProvider;
@@ -980,46 +976,23 @@ pub struct DisableAiSettings {
     pub disable_ai: bool,
 }
 
-#[derive(
-    Copy,
-    Clone,
-    PartialEq,
-    Eq,
-    Debug,
-    Default,
-    serde::Serialize,
-    serde::Deserialize,
-    SettingsUi,
-    SettingsKey,
-    JsonSchema,
-)]
-#[settings_key(None)]
-pub struct DisableAiSettingContent {
-    pub disable_ai: Option<bool>,
-}
-
 impl settings::Settings for DisableAiSettings {
-    type FileContent = DisableAiSettingContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        // For security reasons, settings can only make AI restrictions MORE strict, not less.
-        // (For example, if someone is working on a project that contractually
-        // requires no AI use, that should override the user's setting which
-        // permits AI use.)
-        // This also prevents an attacker from using project or server settings to enable AI when it should be disabled.
-        let disable_ai = sources
-            .project
-            .iter()
-            .chain(sources.user.iter())
-            .chain(sources.server.iter())
-            .chain(sources.release_channel.iter())
-            .chain(sources.profile.iter())
-            .any(|disabled| disabled.disable_ai == Some(true));
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        Self {
+            disable_ai: content.disable_ai.unwrap(),
+        }
+    }
 
-        Ok(Self { disable_ai })
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        // If disable_ai is true *in any file*, it is disabled.
+        self.disable_ai = self.disable_ai || content.disable_ai.unwrap_or(false);
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn import_from_vscode(
+        _vscode: &settings::VsCodeSettings,
+        _current: &mut settings::SettingsContent,
+    ) {
+    }
 }
 
 impl Project {
@@ -3271,7 +3244,6 @@ impl Project {
     ) {
         self.buffers_needing_diff.insert(buffer.downgrade());
         let first_insertion = self.buffers_needing_diff.len() == 1;
-
         let settings = ProjectSettings::get_global(cx);
         let delay = if let Some(delay) = settings.git.gutter_debounce {
             delay
@@ -5668,146 +5640,50 @@ fn provide_inline_values(
 mod disable_ai_settings_tests {
     use super::*;
     use gpui::TestAppContext;
-    use settings::{Settings, SettingsSources};
+    use settings::Settings;
 
     #[gpui::test]
     async fn test_disable_ai_settings_security(cx: &mut TestAppContext) {
-        fn disable_setting(value: Option<bool>) -> DisableAiSettingContent {
-            DisableAiSettingContent { disable_ai: value }
-        }
         cx.update(|cx| {
+            settings::init(cx);
+            Project::init_settings(cx);
+
             // Test 1: Default is false (AI enabled)
-            let sources = SettingsSources {
-                default: &DisableAiSettingContent {
-                    disable_ai: Some(false),
-                },
-                global: None,
-                extensions: None,
-                user: None,
-                release_channel: None,
-                operating_system: None,
-                profile: None,
-                server: None,
-                project: &[],
-            };
-            let settings = DisableAiSettings::load(sources, cx).unwrap();
-            assert!(!settings.disable_ai, "Default should allow AI");
-
-            // Test 2: Global true, local false -> still disabled (local cannot re-enable)
-            let global_true = disable_setting(Some(true));
-            let local_false = disable_setting(Some(false));
-            let sources = SettingsSources {
-                default: &disable_setting(Some(false)),
-                global: None,
-                extensions: None,
-                user: Some(&global_true),
-                release_channel: None,
-                operating_system: None,
-                profile: None,
-                server: None,
-                project: &[&local_false],
-            };
-            let settings = DisableAiSettings::load(sources, cx).unwrap();
             assert!(
-                settings.disable_ai,
-                "Local false cannot override global true"
+                !DisableAiSettings::get_global(cx).disable_ai,
+                "Default should allow AI"
             );
+        });
 
-            // Test 3: Global false, local true -> disabled (local can make more restrictive)
-            let global_false = disable_setting(Some(false));
-            let local_true = disable_setting(Some(true));
-            let sources = SettingsSources {
-                default: &disable_setting(Some(false)),
-                global: None,
-                extensions: None,
-                user: Some(&global_false),
-                release_channel: None,
-                operating_system: None,
-                profile: None,
-                server: None,
-                project: &[&local_true],
-            };
-            let settings = DisableAiSettings::load(sources, cx).unwrap();
-            assert!(settings.disable_ai, "Local true can override global false");
-
-            // Test 4: Server can only make more restrictive (set to true)
-            let user_false = disable_setting(Some(false));
-            let server_true = disable_setting(Some(true));
-            let sources = SettingsSources {
-                default: &disable_setting(Some(false)),
-                global: None,
-                extensions: None,
-                user: Some(&user_false),
-                release_channel: None,
-                operating_system: None,
-                profile: None,
-                server: Some(&server_true),
-                project: &[],
-            };
-            let settings = DisableAiSettings::load(sources, cx).unwrap();
-            assert!(
-                settings.disable_ai,
-                "Server can set to true even if user is false"
-            );
+        let disable_true = serde_json::json!({
+            "disable_ai": true
+        })
+        .to_string();
+        let disable_false = serde_json::json!({
+            "disable_ai": false
+        })
+        .to_string();
 
-            // Test 5: Server false cannot override user true
-            let user_true = disable_setting(Some(true));
-            let server_false = disable_setting(Some(false));
-            let sources = SettingsSources {
-                default: &disable_setting(Some(false)),
-                global: None,
-                extensions: None,
-                user: Some(&user_true),
-                release_channel: None,
-                operating_system: None,
-                profile: None,
-                server: Some(&server_false),
-                project: &[],
-            };
-            let settings = DisableAiSettings::load(sources, cx).unwrap();
+        cx.update_global::<SettingsStore, _>(|store, cx| {
+            store.set_user_settings(&disable_false, cx).unwrap();
+            store.set_global_settings(&disable_true, cx).unwrap();
+        });
+        cx.update(|cx| {
             assert!(
-                settings.disable_ai,
-                "Server false cannot override user true"
+                DisableAiSettings::get_global(cx).disable_ai,
+                "Local false cannot override global true"
             );
+        });
 
-            // Test 6: Multiple local settings, any true disables AI
-            let global_false = disable_setting(Some(false));
-            let local_false3 = disable_setting(Some(false));
-            let local_true2 = disable_setting(Some(true));
-            let local_false4 = disable_setting(Some(false));
-            let sources = SettingsSources {
-                default: &disable_setting(Some(false)),
-                global: None,
-                extensions: None,
-                user: Some(&global_false),
-                release_channel: None,
-                operating_system: None,
-                profile: None,
-                server: None,
-                project: &[&local_false3, &local_true2, &local_false4],
-            };
-            let settings = DisableAiSettings::load(sources, cx).unwrap();
-            assert!(settings.disable_ai, "Any local true should disable AI");
-
-            // Test 7: All three sources can independently disable AI
-            let user_false2 = disable_setting(Some(false));
-            let server_false2 = disable_setting(Some(false));
-            let local_true3 = disable_setting(Some(true));
-            let sources = SettingsSources {
-                default: &disable_setting(Some(false)),
-                global: None,
-                extensions: None,
-                user: Some(&user_false2),
-                release_channel: None,
-                operating_system: None,
-                profile: None,
-                server: Some(&server_false2),
-                project: &[&local_true3],
-            };
-            let settings = DisableAiSettings::load(sources, cx).unwrap();
+        cx.update_global::<SettingsStore, _>(|store, cx| {
+            store.set_global_settings(&disable_false, cx).unwrap();
+            store.set_user_settings(&disable_true, cx).unwrap();
+        });
+
+        cx.update(|cx| {
             assert!(
-                settings.disable_ai,
-                "Local can disable even if user and server are false"
+                DisableAiSettings::get_global(cx).disable_ai,
+                "Local false cannot override global true"
             );
         });
     }

crates/project/src/project_settings.rs πŸ”—

@@ -17,18 +17,19 @@ use rpc::{
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+pub use settings::DirenvSettings;
+pub use settings::LspSettings;
 use settings::{
-    InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation,
-    SettingsSources, SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file,
+    DapSettingsContent, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
+    SettingsStore, SettingsUi, parse_json_with_comments, watch_config_file,
 };
 use std::{
-    collections::BTreeMap,
     path::{Path, PathBuf},
     sync::Arc,
     time::Duration,
 };
 use task::{DebugTaskFile, TaskTemplates, VsCodeDebugTaskFile, VsCodeTaskFile};
-use util::{ResultExt, serde::default_true};
+use util::{MergeFrom as _, ResultExt, serde::default_true};
 use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
 
 use crate::{
@@ -36,8 +37,7 @@ use crate::{
     worktree_store::{WorktreeStore, WorktreeStoreEvent},
 };
 
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
+#[derive(Debug, Clone)]
 pub struct ProjectSettings {
     /// Configuration for language servers.
     ///
@@ -47,48 +47,75 @@ pub struct ProjectSettings {
     /// To override settings for a language, add an entry for that language server's
     /// name to the lsp value.
     /// Default: null
-    #[serde(default)]
-    pub lsp: HashMap<LanguageServerName, LspSettings>,
+    // todo(settings-follow-up)
+    // We should change to use a non content type (settings::LspSettings is a content type)
+    // Note: Will either require merging with defaults, which also requires deciding where the defaults come from,
+    //       or case by case deciding which fields are optional and which are actually required.
+    pub lsp: HashMap<LanguageServerName, settings::LspSettings>,
 
     /// Common language server settings.
-    #[serde(default)]
     pub global_lsp_settings: GlobalLspSettings,
 
     /// Configuration for Debugger-related features
-    #[serde(default)]
     pub dap: HashMap<DebugAdapterName, DapSettings>,
 
     /// Settings for context servers used for AI-related features.
-    #[serde(default)]
     pub context_servers: HashMap<Arc<str>, ContextServerSettings>,
 
     /// Configuration for Diagnostics-related features.
-    #[serde(default)]
     pub diagnostics: DiagnosticsSettings,
 
     /// Configuration for Git-related features
-    #[serde(default)]
     pub git: GitSettings,
 
     /// Configuration for Node-related features
-    #[serde(default)]
     pub node: NodeBinarySettings,
 
     /// Configuration for how direnv configuration should be loaded
-    #[serde(default)]
     pub load_direnv: DirenvSettings,
 
     /// Configuration for session-related features
-    #[serde(default)]
     pub session: SessionSettings,
 }
 
+#[derive(Copy, Clone, Debug)]
+pub struct SessionSettings {
+    /// Whether or not to restore unsaved buffers on restart.
+    ///
+    /// If this is true, user won't be prompted whether to save/discard
+    /// dirty files when closing the application.
+    ///
+    /// Default: true
+    pub restore_unsaved_buffers: bool,
+}
+
 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub struct DapSettings {
-    pub binary: Option<String>,
-    #[serde(default)]
-    pub args: Vec<String>,
+pub struct NodeBinarySettings {
+    /// The path to the Node binary.
+    pub path: Option<String>,
+    /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
+    pub npm_path: Option<String>,
+    /// If enabled, Zed will download its own copy of Node.
+    pub ignore_system_version: bool,
+}
+
+impl From<settings::NodeBinarySettings> for NodeBinarySettings {
+    fn from(settings: settings::NodeBinarySettings) -> Self {
+        Self {
+            path: settings.path,
+            npm_path: settings.npm_path,
+            ignore_system_version: settings.ignore_system_version.unwrap_or(false),
+        }
+    }
+}
+
+/// Common language server settings.
+#[derive(Debug, Clone, PartialEq)]
+pub struct GlobalLspSettings {
+    /// Whether to show the LSP servers button in the status bar.
+    ///
+    /// Default: `true`
+    pub button: bool,
 }
 
 #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
@@ -114,14 +141,29 @@ pub enum ContextServerSettings {
     },
 }
 
-/// Common language server settings.
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct GlobalLspSettings {
-    /// Whether to show the LSP servers button in the status bar.
-    ///
-    /// Default: `true`
-    #[serde(default = "default_true")]
-    pub button: bool,
+impl From<settings::ContextServerSettingsContent> for ContextServerSettings {
+    fn from(value: settings::ContextServerSettingsContent) -> Self {
+        match value {
+            settings::ContextServerSettingsContent::Custom { enabled, command } => {
+                ContextServerSettings::Custom { enabled, command }
+            }
+            settings::ContextServerSettingsContent::Extension { enabled, settings } => {
+                ContextServerSettings::Extension { enabled, settings }
+            }
+        }
+    }
+}
+impl Into<settings::ContextServerSettingsContent> for ContextServerSettings {
+    fn into(self) -> settings::ContextServerSettingsContent {
+        match self {
+            ContextServerSettings::Custom { enabled, command } => {
+                settings::ContextServerSettingsContent::Custom { enabled, command }
+            }
+            ContextServerSettings::Extension { enabled, settings } => {
+                settings::ContextServerSettingsContent::Extension { enabled, settings }
+            }
+        }
+    }
 }
 
 impl ContextServerSettings {
@@ -147,140 +189,6 @@ impl ContextServerSettings {
     }
 }
 
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-pub struct NodeBinarySettings {
-    /// The path to the Node binary.
-    pub path: Option<String>,
-    /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
-    pub npm_path: Option<String>,
-    /// If enabled, Zed will download its own copy of Node.
-    #[serde(default)]
-    pub ignore_system_version: bool,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum DirenvSettings {
-    /// Load direnv configuration through a shell hook
-    ShellHook,
-    /// Load direnv configuration directly using `direnv export json`
-    #[default]
-    Direct,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(default)]
-pub struct DiagnosticsSettings {
-    /// Whether to show the project diagnostics button in the status bar.
-    pub button: bool,
-
-    /// Whether or not to include warning diagnostics.
-    pub include_warnings: bool,
-
-    /// Settings for using LSP pull diagnostics mechanism in Zed.
-    pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
-
-    /// Settings for showing inline diagnostics.
-    pub inline: InlineDiagnosticsSettings,
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(default)]
-pub struct LspPullDiagnosticsSettings {
-    /// Whether to pull for diagnostics or not.
-    ///
-    /// Default: true
-    #[serde(default = "default_true")]
-    pub enabled: bool,
-    /// Minimum time to wait before pulling diagnostics from the language server(s).
-    /// 0 turns the debounce off.
-    ///
-    /// Default: 50
-    #[serde(default = "default_lsp_diagnostics_pull_debounce_ms")]
-    pub debounce_ms: u64,
-}
-
-fn default_lsp_diagnostics_pull_debounce_ms() -> u64 {
-    50
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(default)]
-pub struct InlineDiagnosticsSettings {
-    /// Whether or not to show inline diagnostics
-    ///
-    /// Default: false
-    pub enabled: bool,
-    /// Whether to only show the inline diagnostics after a delay after the
-    /// last editor event.
-    ///
-    /// Default: 150
-    #[serde(default = "default_inline_diagnostics_update_debounce_ms")]
-    pub update_debounce_ms: u64,
-    /// The amount of padding between the end of the source line and the start
-    /// of the inline diagnostic in units of columns.
-    ///
-    /// Default: 4
-    #[serde(default = "default_inline_diagnostics_padding")]
-    pub padding: u32,
-    /// The minimum column to display inline diagnostics. This setting can be
-    /// used to horizontally align inline diagnostics at some position. Lines
-    /// longer than this value will still push diagnostics further to the right.
-    ///
-    /// Default: 0
-    pub min_column: u32,
-
-    pub max_severity: Option<DiagnosticSeverity>,
-}
-
-fn default_inline_diagnostics_update_debounce_ms() -> u64 {
-    150
-}
-
-fn default_inline_diagnostics_padding() -> u32 {
-    4
-}
-
-impl Default for DiagnosticsSettings {
-    fn default() -> Self {
-        Self {
-            button: true,
-            include_warnings: true,
-            lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(),
-            inline: InlineDiagnosticsSettings::default(),
-        }
-    }
-}
-
-impl Default for LspPullDiagnosticsSettings {
-    fn default() -> Self {
-        Self {
-            enabled: true,
-            debounce_ms: default_lsp_diagnostics_pull_debounce_ms(),
-        }
-    }
-}
-
-impl Default for InlineDiagnosticsSettings {
-    fn default() -> Self {
-        Self {
-            enabled: false,
-            update_debounce_ms: default_inline_diagnostics_update_debounce_ms(),
-            padding: default_inline_diagnostics_padding(),
-            min_column: 0,
-            max_severity: None,
-        }
-    }
-}
-
-impl Default for GlobalLspSettings {
-    fn default() -> Self {
-        Self {
-            button: default_true(),
-        }
-    }
-}
-
 #[derive(
     Clone,
     Copy,
@@ -301,7 +209,6 @@ pub enum DiagnosticSeverity {
     Error,
     Warning,
     Info,
-    #[serde(alias = "all")]
     Hint,
 }
 
@@ -317,6 +224,18 @@ impl DiagnosticSeverity {
     }
 }
 
+impl From<settings::DiagnosticSeverityContent> for DiagnosticSeverity {
+    fn from(severity: settings::DiagnosticSeverityContent) -> Self {
+        match severity {
+            settings::DiagnosticSeverityContent::Off => DiagnosticSeverity::Off,
+            settings::DiagnosticSeverityContent::Error => DiagnosticSeverity::Error,
+            settings::DiagnosticSeverityContent::Warning => DiagnosticSeverity::Warning,
+            settings::DiagnosticSeverityContent::Info => DiagnosticSeverity::Info,
+            settings::DiagnosticSeverityContent::Hint => DiagnosticSeverity::Hint,
+        }
+    }
+}
+
 /// Determines the severity of the diagnostic that should be moved to.
 #[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
@@ -390,12 +309,12 @@ impl GoToDiagnosticSeverityFilter {
     }
 }
 
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Copy, Clone, Debug)]
 pub struct GitSettings {
     /// Whether or not to show the git gutter.
     ///
     /// Default: tracked_files
-    pub git_gutter: Option<GitGutterSetting>,
+    pub git_gutter: settings::GitGutterSetting,
     /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
     ///
     /// Default: null
@@ -404,111 +323,50 @@ pub struct GitSettings {
     /// the currently focused line.
     ///
     /// Default: on
-    pub inline_blame: Option<InlineBlameSettings>,
+    pub inline_blame: InlineBlameSettings,
     /// Which information to show in the branch picker.
     ///
     /// Default: on
-    pub branch_picker: Option<BranchPickerSettings>,
+    pub branch_picker: BranchPickerSettings,
     /// How hunks are displayed visually in the editor.
     ///
     /// Default: staged_hollow
-    pub hunk_style: Option<GitHunkStyleSetting>,
+    pub hunk_style: settings::GitHunkStyleSetting,
 }
 
-impl GitSettings {
-    pub fn inline_blame_enabled(&self) -> bool {
-        #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
-        match self.inline_blame {
-            Some(InlineBlameSettings { enabled, .. }) => enabled,
-            _ => false,
-        }
-    }
-
-    pub fn inline_blame_delay(&self) -> Option<Duration> {
-        match self.inline_blame {
-            Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => {
-                Some(Duration::from_millis(delay_ms))
-            }
-            _ => None,
-        }
-    }
-
-    pub fn show_inline_commit_summary(&self) -> bool {
-        match self.inline_blame {
-            Some(InlineBlameSettings {
-                show_commit_summary,
-                ..
-            }) => show_commit_summary,
-            _ => false,
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitHunkStyleSetting {
-    /// Show unstaged hunks with a filled background and staged hunks hollow.
-    #[default]
-    StagedHollow,
-    /// Show unstaged hunks hollow and staged hunks with a filled background.
-    UnstagedHollow,
-}
-
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitGutterSetting {
-    /// Show git gutter in tracked files.
-    #[default]
-    TrackedFiles,
-    /// Hide git gutter
-    Hide,
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
+#[derive(Clone, Copy, Debug)]
 pub struct InlineBlameSettings {
     /// Whether or not to show git blame data inline in
     /// the currently focused line.
     ///
     /// Default: true
-    #[serde(default = "default_true")]
     pub enabled: bool,
     /// Whether to only show the inline blame information
     /// after a delay once the cursor stops moving.
     ///
     /// Default: 0
-    #[serde(default)]
-    pub delay_ms: u64,
+    pub delay_ms: std::time::Duration,
     /// The amount of padding between the end of the source line and the start
     /// of the inline blame in units of columns.
     ///
     /// Default: 7
-    #[serde(default = "default_inline_blame_padding")]
     pub padding: u32,
     /// The minimum column number to show the inline blame information at
     ///
     /// Default: 0
-    #[serde(default)]
     pub min_column: u32,
     /// Whether to show commit summary as part of the inline blame.
     ///
     /// Default: false
-    #[serde(default)]
     pub show_commit_summary: bool,
 }
 
-fn default_inline_blame_padding() -> u32 {
-    7
-}
-
-impl Default for InlineBlameSettings {
-    fn default() -> Self {
-        Self {
-            enabled: true,
-            delay_ms: 0,
-            padding: default_inline_blame_padding(),
-            min_column: 0,
-            show_commit_summary: false,
+impl GitSettings {
+    pub fn inline_blame_delay(&self) -> Option<Duration> {
+        if self.inline_blame.delay_ms.as_millis() > 0 {
+            Some(self.inline_blame.delay_ms)
+        } else {
+            None
         }
     }
 }
@@ -531,93 +389,273 @@ impl Default for BranchPickerSettings {
     }
 }
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
-pub struct BinarySettings {
-    pub path: Option<String>,
-    pub arguments: Option<Vec<String>>,
-    pub env: Option<BTreeMap<String, String>>,
-    pub ignore_system_version: Option<bool>,
-}
+#[derive(Clone, Debug)]
+pub struct DiagnosticsSettings {
+    /// Whether to show the project diagnostics button in the status bar.
+    pub button: bool,
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
-pub struct FetchSettings {
-    // Whether to consider pre-releases for fetching
-    pub pre_release: Option<bool>,
-}
+    /// Whether or not to include warning diagnostics.
+    pub include_warnings: bool,
 
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
-#[serde(rename_all = "snake_case")]
-pub struct LspSettings {
-    pub binary: Option<BinarySettings>,
-    pub initialization_options: Option<serde_json::Value>,
-    pub settings: Option<serde_json::Value>,
-    /// If the server supports sending tasks over LSP extensions,
-    /// this setting can be used to enable or disable them in Zed.
-    /// Default: true
-    #[serde(default = "default_true")]
-    pub enable_lsp_tasks: bool,
-    pub fetch: Option<FetchSettings>,
-}
+    /// Settings for using LSP pull diagnostics mechanism in Zed.
+    pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
 
-impl Default for LspSettings {
-    fn default() -> Self {
-        Self {
-            binary: None,
-            initialization_options: None,
-            settings: None,
-            enable_lsp_tasks: true,
-            fetch: None,
-        }
-    }
+    /// Settings for showing inline diagnostics.
+    pub inline: InlineDiagnosticsSettings,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
-pub struct SessionSettings {
-    /// Whether or not to restore unsaved buffers on restart.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct InlineDiagnosticsSettings {
+    /// Whether or not to show inline diagnostics
     ///
-    /// If this is true, user won't be prompted whether to save/discard
-    /// dirty files when closing the application.
+    /// Default: false
+    pub enabled: bool,
+    /// Whether to only show the inline diagnostics after a delay after the
+    /// last editor event.
+    ///
+    /// Default: 150
+    pub update_debounce_ms: u64,
+    /// The amount of padding between the end of the source line and the start
+    /// of the inline diagnostic in units of columns.
+    ///
+    /// Default: 4
+    pub padding: u32,
+    /// The minimum column to display inline diagnostics. This setting can be
+    /// used to horizontally align inline diagnostics at some position. Lines
+    /// longer than this value will still push diagnostics further to the right.
+    ///
+    /// Default: 0
+    pub min_column: u32,
+
+    pub max_severity: Option<DiagnosticSeverity>,
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct LspPullDiagnosticsSettings {
+    /// Whether to pull for diagnostics or not.
     ///
     /// Default: true
-    pub restore_unsaved_buffers: bool,
+    pub enabled: bool,
+    /// Minimum time to wait before pulling diagnostics from the language server(s).
+    /// 0 turns the debounce off.
+    ///
+    /// Default: 50
+    pub debounce_ms: u64,
 }
 
-impl Default for SessionSettings {
-    fn default() -> Self {
+impl Settings for ProjectSettings {
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let project = &content.project.clone();
+        let diagnostics = content.diagnostics.as_ref().unwrap();
+        let lsp_pull_diagnostics = diagnostics.lsp_pull_diagnostics.as_ref().unwrap();
+        let inline_diagnostics = diagnostics.inline.as_ref().unwrap();
+
+        let git = content.git.as_ref().unwrap();
+        let git_settings = GitSettings {
+            git_gutter: git.git_gutter.unwrap(),
+            gutter_debounce: git.gutter_debounce,
+            inline_blame: {
+                let inline = git.inline_blame.unwrap();
+                InlineBlameSettings {
+                    enabled: inline.enabled.unwrap(),
+                    delay_ms: std::time::Duration::from_millis(inline.delay_ms.unwrap()),
+                    padding: inline.padding.unwrap(),
+                    min_column: inline.min_column.unwrap(),
+                    show_commit_summary: inline.show_commit_summary.unwrap(),
+                }
+            },
+            branch_picker: {
+                let branch_picker = git.branch_picker.unwrap();
+                BranchPickerSettings {
+                    show_author_name: branch_picker.show_author_name.unwrap(),
+                }
+            },
+            hunk_style: git.hunk_style.unwrap(),
+        };
         Self {
-            restore_unsaved_buffers: true,
+            context_servers: project
+                .context_servers
+                .clone()
+                .into_iter()
+                .map(|(key, value)| (key, value.into()))
+                .collect(),
+            lsp: project
+                .lsp
+                .clone()
+                .into_iter()
+                .map(|(key, value)| (LanguageServerName(key.into()), value))
+                .collect(),
+            global_lsp_settings: GlobalLspSettings {
+                button: content
+                    .global_lsp_settings
+                    .as_ref()
+                    .unwrap()
+                    .button
+                    .unwrap(),
+            },
+            dap: project
+                .dap
+                .clone()
+                .into_iter()
+                .map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value)))
+                .collect(),
+            diagnostics: DiagnosticsSettings {
+                button: diagnostics.button.unwrap(),
+                include_warnings: diagnostics.include_warnings.unwrap(),
+                lsp_pull_diagnostics: LspPullDiagnosticsSettings {
+                    enabled: lsp_pull_diagnostics.enabled.unwrap(),
+                    debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap(),
+                },
+                inline: InlineDiagnosticsSettings {
+                    enabled: inline_diagnostics.enabled.unwrap(),
+                    update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(),
+                    padding: inline_diagnostics.padding.unwrap(),
+                    min_column: inline_diagnostics.min_column.unwrap(),
+                    max_severity: inline_diagnostics.max_severity.map(Into::into),
+                },
+            },
+            git: git_settings,
+            node: content.node.clone().unwrap().into(),
+            load_direnv: project.load_direnv.clone().unwrap(),
+            session: SessionSettings {
+                restore_unsaved_buffers: content.session.unwrap().restore_unsaved_buffers.unwrap(),
+            },
         }
     }
-}
 
-impl Settings for ProjectSettings {
-    type FileContent = Self;
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let project = &content.project;
+        self.context_servers.extend(
+            project
+                .context_servers
+                .clone()
+                .into_iter()
+                .map(|(key, value)| (key, value.into())),
+        );
+        self.dap.extend(
+            project
+                .dap
+                .clone()
+                .into_iter()
+                .map(|(key, value)| (DebugAdapterName(key.into()), DapSettings::from(value))),
+        );
+        if let Some(diagnostics) = content.diagnostics.as_ref() {
+            if let Some(inline) = &diagnostics.inline {
+                self.diagnostics.inline.enabled.merge_from(&inline.enabled);
+                self.diagnostics
+                    .inline
+                    .update_debounce_ms
+                    .merge_from(&inline.update_debounce_ms);
+                self.diagnostics.inline.padding.merge_from(&inline.padding);
+                self.diagnostics
+                    .inline
+                    .min_column
+                    .merge_from(&inline.min_column);
+                if let Some(max_severity) = inline.max_severity {
+                    self.diagnostics.inline.max_severity = Some(max_severity.into())
+                }
+            }
+
+            self.diagnostics.button.merge_from(&diagnostics.button);
+            self.diagnostics
+                .include_warnings
+                .merge_from(&diagnostics.include_warnings);
+            if let Some(pull_diagnostics) = &diagnostics.lsp_pull_diagnostics {
+                self.diagnostics
+                    .lsp_pull_diagnostics
+                    .enabled
+                    .merge_from(&pull_diagnostics.enabled);
+                self.diagnostics
+                    .lsp_pull_diagnostics
+                    .debounce_ms
+                    .merge_from(&pull_diagnostics.debounce_ms);
+            }
+        }
+        if let Some(git) = content.git.as_ref() {
+            if let Some(branch_picker) = git.branch_picker.as_ref() {
+                self.git
+                    .branch_picker
+                    .show_author_name
+                    .merge_from(&branch_picker.show_author_name);
+            }
+            if let Some(inline_blame) = git.inline_blame.as_ref() {
+                self.git
+                    .inline_blame
+                    .enabled
+                    .merge_from(&inline_blame.enabled);
+                self.git
+                    .inline_blame
+                    .delay_ms
+                    .merge_from(&inline_blame.delay_ms.map(std::time::Duration::from_millis));
+                self.git
+                    .inline_blame
+                    .padding
+                    .merge_from(&inline_blame.padding);
+                self.git
+                    .inline_blame
+                    .min_column
+                    .merge_from(&inline_blame.min_column);
+                self.git
+                    .inline_blame
+                    .show_commit_summary
+                    .merge_from(&inline_blame.show_commit_summary);
+            }
+            self.git.git_gutter.merge_from(&git.git_gutter);
+            self.git.hunk_style.merge_from(&git.hunk_style);
+            if let Some(debounce) = git.gutter_debounce {
+                self.git.gutter_debounce = Some(debounce);
+            }
+        }
+        self.global_lsp_settings.button.merge_from(
+            &content
+                .global_lsp_settings
+                .as_ref()
+                .and_then(|settings| settings.button),
+        );
+        self.load_direnv
+            .merge_from(&content.project.load_direnv.clone());
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
-        sources.json_merge()
+        for (key, value) in content.project.lsp.clone() {
+            self.lsp.insert(LanguageServerName(key.into()), value);
+        }
+
+        if let Some(node) = content.node.as_ref() {
+            self.node
+                .ignore_system_version
+                .merge_from(&node.ignore_system_version);
+            if let Some(path) = node.path.clone() {
+                self.node.path = Some(path);
+            }
+            if let Some(npm_path) = node.npm_path.clone() {
+                self.node.npm_path = Some(npm_path);
+            }
+        }
+        self.session
+            .restore_unsaved_buffers
+            .merge_from(&content.session.and_then(|s| s.restore_unsaved_buffers));
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
+    fn import_from_vscode(
+        vscode: &settings::VsCodeSettings,
+        current: &mut settings::SettingsContent,
+    ) {
         // this just sets the binary name instead of a full path so it relies on path lookup
         // resolving to the one you want
-        vscode.enum_setting(
-            "npm.packageManager",
-            &mut current.node.npm_path,
-            |s| match s {
-                v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
-                _ => None,
-            },
-        );
+        let npm_path = vscode.read_enum("npm.packageManager", |s| match s {
+            v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()),
+            _ => None,
+        });
+        if npm_path.is_some() {
+            current.node.get_or_insert_default().npm_path = npm_path;
+        }
 
         if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") {
-            if let Some(blame) = current.git.inline_blame.as_mut() {
-                blame.enabled = b
-            } else {
-                current.git.inline_blame = Some(InlineBlameSettings {
-                    enabled: b,
-                    ..Default::default()
-                })
-            }
+            current
+                .git
+                .get_or_insert_default()
+                .inline_blame
+                .get_or_insert_default()
+                .enabled = Some(b);
         }
 
         #[derive(Deserialize)]
@@ -627,29 +665,27 @@ impl Settings for ProjectSettings {
             env: Option<HashMap<String, String>>,
             // note: we don't support envFile and type
         }
-        impl From<VsCodeContextServerCommand> for ContextServerCommand {
-            fn from(cmd: VsCodeContextServerCommand) -> Self {
-                Self {
-                    path: cmd.command,
-                    args: cmd.args.unwrap_or_default(),
-                    env: cmd.env,
-                    timeout: None,
-                }
-            }
-        }
         if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) {
             current
+                .project
                 .context_servers
                 .extend(mcp.iter().filter_map(|(k, v)| {
                     Some((
                         k.clone().into(),
-                        ContextServerSettings::Custom {
+                        settings::ContextServerSettingsContent::Custom {
                             enabled: true,
                             command: serde_json::from_value::<VsCodeContextServerCommand>(
                                 v.clone(),
                             )
-                            .ok()?
-                            .into(),
+                            .ok()
+                            .map(|cmd| {
+                                settings::ContextServerCommand {
+                                    path: cmd.command,
+                                    args: cmd.args.unwrap_or_default(),
+                                    env: cmd.env,
+                                    timeout: None,
+                                }
+                            })?,
                         },
                     ))
                 }));
@@ -736,17 +772,19 @@ impl SettingsObserver {
             if let Some(upstream_client) = upstream_client {
                 let mut user_settings = None;
                 user_settings_watcher = Some(cx.observe_global::<SettingsStore>(move |_, cx| {
-                    let new_settings = cx.global::<SettingsStore>().raw_user_settings();
-                    if Some(new_settings) != user_settings.as_ref() {
-                        if let Some(new_settings_string) = serde_json::to_string(new_settings).ok()
-                        {
-                            user_settings = Some(new_settings.clone());
-                            upstream_client
-                                .send(proto::UpdateUserSettings {
-                                    project_id: REMOTE_SERVER_PROJECT_ID,
-                                    contents: new_settings_string,
-                                })
-                                .log_err();
+                    if let Some(new_settings) = cx.global::<SettingsStore>().raw_user_settings() {
+                        if Some(new_settings) != user_settings.as_ref() {
+                            if let Some(new_settings_string) =
+                                serde_json::to_string(new_settings).ok()
+                            {
+                                user_settings = Some(new_settings.clone());
+                                upstream_client
+                                    .send(proto::UpdateUserSettings {
+                                        project_id: REMOTE_SERVER_PROJECT_ID,
+                                        contents: new_settings_string,
+                                    })
+                                    .log_err();
+                            }
                         }
                     }
                 }));
@@ -786,6 +824,7 @@ impl SettingsObserver {
         for worktree in self.worktree_store.read(cx).worktrees() {
             let worktree_id = worktree.read(cx).id().to_proto();
             for (path, content) in store.local_settings(worktree.read(cx).id()) {
+                let content = serde_json::to_string(&content).unwrap();
                 downstream_client
                     .send(proto::UpdateWorktreeSettings {
                         project_id,
@@ -856,10 +895,9 @@ impl SettingsObserver {
         envelope: TypedEnvelope<proto::UpdateUserSettings>,
         cx: AsyncApp,
     ) -> anyhow::Result<()> {
-        let new_settings = serde_json::from_str::<serde_json::Value>(&envelope.payload.contents)
-            .with_context(|| {
-                format!("deserializing {} user settings", envelope.payload.contents)
-            })?;
+        let new_settings = serde_json::from_str(&envelope.payload.contents).with_context(|| {
+            format!("deserializing {} user settings", envelope.payload.contents)
+        })?;
         cx.update_global(|settings_store: &mut SettingsStore, cx| {
             settings_store
                 .set_raw_user_settings(new_settings, cx)
@@ -1292,3 +1330,26 @@ pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSett
         LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
     }
 }
+
+#[derive(Debug, Clone)]
+pub struct DapSettings {
+    pub binary: DapBinary,
+    pub args: Vec<String>,
+}
+
+impl From<DapSettingsContent> for DapSettings {
+    fn from(content: DapSettingsContent) -> Self {
+        DapSettings {
+            binary: content
+                .binary
+                .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)),
+            args: content.args.unwrap_or_default(),
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub enum DapBinary {
+    Default,
+    Custom(String),
+}

crates/project/src/project_tests.rs πŸ”—

@@ -23,7 +23,7 @@ use language::{
     Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter,
     LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider,
     ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainList, ToolchainLister,
-    language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings},
+    language_settings::{LanguageSettingsContent, language_settings},
     tree_sitter_rust, tree_sitter_typescript,
 };
 use lsp::{
@@ -2246,8 +2246,8 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
     // Disable Rust language server, ensuring only that server gets stopped.
     cx.update(|cx| {
         SettingsStore::update_global(cx, |settings, cx| {
-            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.languages.0.insert(
+            settings.update_user_settings(cx, |settings| {
+                settings.languages_mut().insert(
                     "Rust".into(),
                     LanguageSettingsContent {
                         enable_language_server: Some(false),
@@ -2265,16 +2265,16 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
     // former gets started again and that the latter stops.
     cx.update(|cx| {
         SettingsStore::update_global(cx, |settings, cx| {
-            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.languages.0.insert(
-                    LanguageName::new("Rust"),
+            settings.update_user_settings(cx, |settings| {
+                settings.languages_mut().insert(
+                    "Rust".into(),
                     LanguageSettingsContent {
                         enable_language_server: Some(true),
                         ..Default::default()
                     },
                 );
-                settings.languages.0.insert(
-                    LanguageName::new("JavaScript"),
+                settings.languages_mut().insert(
+                    "JavaScript".into(),
                     LanguageSettingsContent {
                         enable_language_server: Some(false),
                         ..Default::default()
@@ -8769,8 +8769,8 @@ async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) {
     init_test(cx);
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions = Some(Vec::new());
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
             });
         });
     });

crates/project_panel/src/project_panel.rs πŸ”—

@@ -36,12 +36,13 @@ use project::{
     project_settings::GoToDiagnosticSeverityFilter,
     relativize_path,
 };
-use project_panel_settings::{
-    ProjectPanelDockPosition, ProjectPanelSettings, ShowDiagnostics, ShowIndentGuides,
-};
+use project_panel_settings::ProjectPanelSettings;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore, update_settings_file};
+use settings::{
+    DockSide, ProjectPanelEntrySpacing, Settings, SettingsStore, ShowDiagnostics, ShowIndentGuides,
+    update_settings_file,
+};
 use smallvec::SmallVec;
 use std::{any::TypeId, time::Instant};
 use std::{
@@ -343,8 +344,14 @@ pub fn init(cx: &mut App) {
 
         workspace.register_action(|workspace, _: &ToggleHideGitIgnore, _, cx| {
             let fs = workspace.app_state().fs.clone();
-            update_settings_file::<ProjectPanelSettings>(fs, cx, move |setting, _| {
-                setting.hide_gitignore = Some(!setting.hide_gitignore.unwrap_or(false));
+            update_settings_file(fs, cx, move |setting, _| {
+                setting.project_panel.get_or_insert_default().hide_gitignore = Some(
+                    !setting
+                        .project_panel
+                        .get_or_insert_default()
+                        .hide_gitignore
+                        .unwrap_or(false),
+                );
             })
         });
 
@@ -4481,8 +4488,8 @@ impl ProjectPanel {
                     .indent_level(depth)
                     .indent_step_size(px(settings.indent_size))
                     .spacing(match settings.entry_spacing {
-                        project_panel_settings::EntrySpacing::Comfortable => ListItemSpacing::Dense,
-                        project_panel_settings::EntrySpacing::Standard => {
+                        ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense,
+                        ProjectPanelEntrySpacing::Standard => {
                             ListItemSpacing::ExtraDense
                         }
                     })
@@ -5760,8 +5767,8 @@ impl EventEmitter<PanelEvent> for ProjectPanel {}
 impl Panel for ProjectPanel {
     fn position(&self, _: &Window, cx: &App) -> DockPosition {
         match ProjectPanelSettings::get_global(cx).dock {
-            ProjectPanelDockPosition::Left => DockPosition::Left,
-            ProjectPanelDockPosition::Right => DockPosition::Right,
+            DockSide::Left => DockPosition::Left,
+            DockSide::Right => DockPosition::Right,
         }
     }
 
@@ -5770,17 +5777,13 @@ impl Panel for ProjectPanel {
     }
 
     fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
-        settings::update_settings_file::<ProjectPanelSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings, _| {
-                let dock = match position {
-                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
-                    DockPosition::Right => ProjectPanelDockPosition::Right,
-                };
-                settings.dock = Some(dock);
-            },
-        );
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            let dock = match position {
+                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
+                DockPosition::Right => DockSide::Right,
+            };
+            settings.project_panel.get_or_insert_default().dock = Some(dock);
+        });
     }
 
     fn size(&self, _: &Window, cx: &App) -> Pixels {

crates/project_panel/src/project_panel_settings.rs πŸ”—

@@ -2,40 +2,23 @@ use editor::EditorSettings;
 use gpui::Pixels;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
-use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
-#[serde(rename_all = "snake_case")]
-pub enum ProjectPanelDockPosition {
-    Left,
-    Right,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowIndentGuides {
-    Always,
-    Never,
-}
-
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum EntrySpacing {
-    /// Comfortable spacing of entries.
-    #[default]
-    Comfortable,
-    /// The standard spacing of entries.
-    Standard,
-}
+use settings::{
+    DockSide, ProjectPanelEntrySpacing, Settings, SettingsContent, ShowDiagnostics,
+    ShowIndentGuides,
+};
+use ui::{
+    px,
+    scrollbars::{ScrollbarVisibility, ShowScrollbar},
+};
+use util::MergeFrom;
 
 #[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
 pub struct ProjectPanelSettings {
     pub button: bool,
     pub hide_gitignore: bool,
     pub default_width: Pixels,
-    pub dock: ProjectPanelDockPosition,
-    pub entry_spacing: EntrySpacing,
+    pub dock: DockSide,
+    pub entry_spacing: ProjectPanelEntrySpacing,
     pub file_icons: bool,
     pub folder_icons: bool,
     pub git_status: bool,
@@ -56,12 +39,6 @@ pub struct IndentGuidesSettings {
     pub show: ShowIndentGuides,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-pub struct IndentGuidesSettingsContent {
-    /// When to show the scrollbar in the project panel.
-    pub show: Option<ShowIndentGuides>,
-}
-
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 pub struct ScrollbarSettings {
     /// When to show the scrollbar in the project panel.
@@ -70,105 +47,6 @@ pub struct ScrollbarSettings {
     pub show: Option<ShowScrollbar>,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-pub struct ScrollbarSettingsContent {
-    /// When to show the scrollbar in the project panel.
-    ///
-    /// Default: inherits editor scrollbar settings
-    pub show: Option<Option<ShowScrollbar>>,
-}
-
-/// Whether to indicate diagnostic errors and/or warnings in project panel items.
-///
-/// Default: all
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowDiagnostics {
-    /// Never mark the diagnostic errors/warnings in the project panel.
-    Off,
-    /// Mark files containing only diagnostic errors in the project panel.
-    Errors,
-    #[default]
-    /// Mark files containing diagnostic errors or warnings in the project panel.
-    All,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "project_panel")]
-pub struct ProjectPanelSettingsContent {
-    /// Whether to show the project panel button in the status bar.
-    ///
-    /// Default: true
-    pub button: Option<bool>,
-    /// Whether to hide gitignore files in the project panel.
-    ///
-    /// Default: false
-    pub hide_gitignore: Option<bool>,
-    /// Customize default width (in pixels) taken by project panel
-    ///
-    /// Default: 240
-    pub default_width: Option<f32>,
-    /// The position of project panel
-    ///
-    /// Default: left
-    pub dock: Option<ProjectPanelDockPosition>,
-    /// Spacing between worktree entries in the project panel.
-    ///
-    /// Default: comfortable
-    pub entry_spacing: Option<EntrySpacing>,
-    /// Whether to show file icons in the project panel.
-    ///
-    /// Default: true
-    pub file_icons: Option<bool>,
-    /// Whether to show folder icons or chevrons for directories in the project panel.
-    ///
-    /// Default: true
-    pub folder_icons: Option<bool>,
-    /// Whether to show the git status in the project panel.
-    ///
-    /// Default: true
-    pub git_status: Option<bool>,
-    /// Amount of indentation (in pixels) for nested items.
-    ///
-    /// Default: 20
-    pub indent_size: Option<f32>,
-    /// Whether to reveal it in the project panel automatically,
-    /// when a corresponding project entry becomes active.
-    /// Gitignored entries are never auto revealed.
-    ///
-    /// Default: true
-    pub auto_reveal_entries: Option<bool>,
-    /// Whether to fold directories automatically
-    /// when directory has only one directory inside.
-    ///
-    /// Default: true
-    pub auto_fold_dirs: Option<bool>,
-    /// Whether the project panel should open on startup.
-    ///
-    /// Default: true
-    pub starts_open: Option<bool>,
-    /// Scrollbar-related settings
-    pub scrollbar: Option<ScrollbarSettingsContent>,
-    /// Which files containing diagnostic errors/warnings to mark in the project panel.
-    ///
-    /// Default: all
-    pub show_diagnostics: Option<ShowDiagnostics>,
-    /// Settings related to indent guides in the project panel.
-    pub indent_guides: Option<IndentGuidesSettingsContent>,
-    /// Whether to hide the root entry when only one folder is open in the window.
-    ///
-    /// Default: false
-    pub hide_root: Option<bool>,
-    /// Whether to stick parent directories at top of the project panel.
-    ///
-    /// Default: true
-    pub sticky_scroll: Option<bool>,
-    /// Whether to enable drag-and-drop operations in the project panel.
-    ///
-    /// Default: true
-    pub drag_and_drop: Option<bool>,
-}
-
 impl ScrollbarVisibility for ProjectPanelSettings {
     fn visibility(&self, cx: &ui::App) -> ShowScrollbar {
         self.scrollbar
@@ -178,32 +56,112 @@ impl ScrollbarVisibility for ProjectPanelSettings {
 }
 
 impl Settings for ProjectPanelSettings {
-    type FileContent = ProjectPanelSettingsContent;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut ui::App) -> Self {
+        let project_panel = content.project_panel.clone().unwrap();
+        Self {
+            button: project_panel.button.unwrap(),
+            hide_gitignore: project_panel.hide_gitignore.unwrap(),
+            default_width: px(project_panel.default_width.unwrap()),
+            dock: project_panel.dock.unwrap(),
+            entry_spacing: project_panel.entry_spacing.unwrap(),
+            file_icons: project_panel.file_icons.unwrap(),
+            folder_icons: project_panel.folder_icons.unwrap(),
+            git_status: project_panel.git_status.unwrap(),
+            indent_size: project_panel.indent_size.unwrap(),
+            indent_guides: IndentGuidesSettings {
+                show: project_panel.indent_guides.unwrap().show.unwrap(),
+            },
+            sticky_scroll: project_panel.sticky_scroll.unwrap(),
+            auto_reveal_entries: project_panel.auto_reveal_entries.unwrap(),
+            auto_fold_dirs: project_panel.auto_fold_dirs.unwrap(),
+            starts_open: project_panel.starts_open.unwrap(),
+            scrollbar: ScrollbarSettings {
+                show: project_panel
+                    .scrollbar
+                    .unwrap()
+                    .show
+                    .flatten()
+                    .map(Into::into),
+            },
+            show_diagnostics: project_panel.show_diagnostics.unwrap(),
+            hide_root: project_panel.hide_root.unwrap(),
+            drag_and_drop: project_panel.drag_and_drop.unwrap(),
+        }
+    }
 
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _: &mut gpui::App,
-    ) -> anyhow::Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &SettingsContent, _cx: &mut ui::App) {
+        let Some(project_panel) = content.project_panel.as_ref() else {
+            return;
+        };
+        self.button.merge_from(&project_panel.button);
+        self.hide_gitignore
+            .merge_from(&project_panel.hide_gitignore);
+        self.default_width
+            .merge_from(&project_panel.default_width.map(px));
+        self.dock.merge_from(&project_panel.dock);
+        self.entry_spacing.merge_from(&project_panel.entry_spacing);
+        self.file_icons.merge_from(&project_panel.file_icons);
+        self.folder_icons.merge_from(&project_panel.folder_icons);
+        self.git_status.merge_from(&project_panel.git_status);
+        self.indent_size.merge_from(&project_panel.indent_size);
+        self.sticky_scroll.merge_from(&project_panel.sticky_scroll);
+        self.auto_reveal_entries
+            .merge_from(&project_panel.auto_reveal_entries);
+        self.auto_fold_dirs
+            .merge_from(&project_panel.auto_fold_dirs);
+        self.starts_open.merge_from(&project_panel.starts_open);
+        self.show_diagnostics
+            .merge_from(&project_panel.show_diagnostics);
+        self.hide_root.merge_from(&project_panel.hide_root);
+        self.drag_and_drop.merge_from(&project_panel.drag_and_drop);
+        if let Some(show) = project_panel
+            .indent_guides
+            .as_ref()
+            .and_then(|indent| indent.show)
+        {
+            self.indent_guides.show = show;
+        }
+        if let Some(show) = project_panel
+            .scrollbar
+            .as_ref()
+            .and_then(|scrollbar| scrollbar.show)
+        {
+            self.scrollbar.show = show.map(Into::into)
+        }
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
-        vscode.bool_setting("explorer.excludeGitIgnore", &mut current.hide_gitignore);
-        vscode.bool_setting("explorer.autoReveal", &mut current.auto_reveal_entries);
-        vscode.bool_setting("explorer.compactFolders", &mut current.auto_fold_dirs);
+    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
+        if let Some(hide_gitignore) = vscode.read_bool("explorer.excludeGitIgnore") {
+            current.project_panel.get_or_insert_default().hide_gitignore = Some(hide_gitignore);
+        }
+        if let Some(auto_reveal) = vscode.read_bool("explorer.autoReveal") {
+            current
+                .project_panel
+                .get_or_insert_default()
+                .auto_reveal_entries = Some(auto_reveal);
+        }
+        if let Some(compact_folders) = vscode.read_bool("explorer.compactFolders") {
+            current.project_panel.get_or_insert_default().auto_fold_dirs = Some(compact_folders);
+        }
 
         if Some(false) == vscode.read_bool("git.decorations.enabled") {
-            current.git_status = Some(false);
+            current.project_panel.get_or_insert_default().git_status = Some(false);
         }
         if Some(false) == vscode.read_bool("problems.decorations.enabled") {
-            current.show_diagnostics = Some(ShowDiagnostics::Off);
+            current
+                .project_panel
+                .get_or_insert_default()
+                .show_diagnostics = Some(ShowDiagnostics::Off);
         }
         if let (Some(false), Some(false)) = (
             vscode.read_bool("explorer.decorations.badges"),
             vscode.read_bool("explorer.decorations.colors"),
         ) {
-            current.git_status = Some(false);
-            current.show_diagnostics = Some(ShowDiagnostics::Off);
+            current.project_panel.get_or_insert_default().git_status = Some(false);
+            current
+                .project_panel
+                .get_or_insert_default()
+                .show_diagnostics = Some(ShowDiagnostics::Off);
         }
     }
 }

crates/project_panel/src/project_panel_tests.rs πŸ”—

@@ -2,7 +2,7 @@ use super::*;
 use collections::HashSet;
 use gpui::{Empty, Entity, TestAppContext, VisualTestContext, WindowHandle};
 use pretty_assertions::assert_eq;
-use project::{FakeFs, WorktreeSettings};
+use project::FakeFs;
 use serde_json::json;
 use settings::SettingsStore;
 use std::path::{Path, PathBuf};
@@ -161,8 +161,8 @@ async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
     init_test(cx);
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
-                worktree_settings.file_scan_exclusions =
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions =
                     Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
             });
         });
@@ -3448,11 +3448,12 @@ async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
     init_test_with_editor(cx);
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
-                worktree_settings.file_scan_exclusions = Some(Vec::new());
-            });
-            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
-                project_panel_settings.auto_reveal_entries = Some(false)
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
+                settings
+                    .project_panel
+                    .get_or_insert_default()
+                    .auto_reveal_entries = Some(false);
             });
         })
     });
@@ -3570,8 +3571,11 @@ async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
 
     cx.update(|_, cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
-                project_panel_settings.auto_reveal_entries = Some(true)
+            store.update_user_settings(cx, |settings| {
+                settings
+                    .project_panel
+                    .get_or_insert_default()
+                    .auto_reveal_entries = Some(true)
             });
         })
     });
@@ -3684,13 +3688,14 @@ async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
     init_test_with_editor(cx);
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
-                worktree_settings.file_scan_exclusions = Some(Vec::new());
-                worktree_settings.file_scan_inclusions =
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
+                settings.project.worktree.file_scan_inclusions =
                     Some(vec!["always_included_but_ignored_dir/*".to_string()]);
-            });
-            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
-                project_panel_settings.auto_reveal_entries = Some(false)
+                settings
+                    .project_panel
+                    .get_or_insert_default()
+                    .auto_reveal_entries = Some(false)
             });
         })
     });
@@ -3759,8 +3764,11 @@ async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
     cx.run_until_parked();
     cx.update(|_, cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
-                project_panel_settings.auto_reveal_entries = Some(true)
+            store.update_user_settings(cx, |settings| {
+                settings
+                    .project_panel
+                    .get_or_insert_default()
+                    .auto_reveal_entries = Some(true)
             });
         })
     });
@@ -3800,11 +3808,12 @@ async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
     init_test_with_editor(cx);
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
-                worktree_settings.file_scan_exclusions = Some(Vec::new());
-            });
-            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
-                project_panel_settings.auto_reveal_entries = Some(false)
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
+                settings
+                    .project_panel
+                    .get_or_insert_default()
+                    .auto_reveal_entries = Some(false)
             });
         })
     });
@@ -4001,8 +4010,8 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) {
     init_test(cx);
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions =
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions =
                     Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
             });
         });
@@ -6650,11 +6659,12 @@ fn init_test(cx: &mut TestAppContext) {
         Project::init_settings(cx);
 
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
-                project_panel_settings.auto_fold_dirs = Some(false);
-            });
-            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
-                worktree_settings.file_scan_exclusions = Some(Vec::new());
+            store.update_user_settings(cx, |settings| {
+                settings
+                    .project_panel
+                    .get_or_insert_default()
+                    .auto_fold_dirs = Some(false);
+                settings.project.worktree.file_scan_exclusions = Some(Vec::new());
             });
         });
     });
@@ -6672,11 +6682,12 @@ fn init_test_with_editor(cx: &mut TestAppContext) {
         Project::init_settings(cx);
 
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
-                project_panel_settings.auto_fold_dirs = Some(false);
-            });
-            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
-                worktree_settings.file_scan_exclusions = Some(Vec::new());
+            store.update_user_settings(cx, |settings| {
+                settings
+                    .project_panel
+                    .get_or_insert_default()
+                    .auto_fold_dirs = Some(false);
+                settings.project.worktree.file_scan_exclusions = Some(Vec::new())
             });
         });
     });

crates/recent_projects/Cargo.toml πŸ”—

@@ -31,7 +31,6 @@ picker.workspace = true
 project.workspace = true
 release_channel.workspace = true
 remote.workspace = true
-schemars.workspace = true
 serde.workspace = true
 settings.workspace = true
 smol.workspace = true

crates/recent_projects/src/recent_projects.rs πŸ”—

@@ -700,7 +700,7 @@ mod tests {
     use dap::debugger_settings::DebuggerSettings;
     use editor::Editor;
     use gpui::{TestAppContext, UpdateGlobal, WindowHandle};
-    use project::{Project, project_settings::ProjectSettings};
+    use project::Project;
     use serde_json::json;
     use settings::SettingsStore;
     use util::path;
@@ -714,8 +714,11 @@ mod tests {
 
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<ProjectSettings>(cx, |settings| {
-                    settings.session.restore_unsaved_buffers = false
+                store.update_user_settings(cx, |settings| {
+                    settings
+                        .session
+                        .get_or_insert_default()
+                        .restore_unsaved_buffers = Some(false)
                 });
             });
         });

crates/recent_projects/src/remote_connections.rs πŸ”—

@@ -1,4 +1,3 @@
-use std::collections::BTreeSet;
 use std::{path::PathBuf, sync::Arc};
 
 use anyhow::{Context as _, Result};
@@ -17,35 +16,32 @@ use markdown::{Markdown, MarkdownElement, MarkdownStyle};
 use release_channel::ReleaseChannel;
 use remote::{
     ConnectionIdentifier, RemoteClient, RemoteConnectionOptions, RemotePlatform,
-    SshConnectionOptions, SshPortForwardOption, WslConnectionOptions,
+    SshConnectionOptions,
 };
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+pub use settings::SshConnection;
+use settings::{Settings, WslConnection};
 use theme::ThemeSettings;
 use ui::{
     ActiveTheme, Color, CommonAnimationExt, Context, Icon, IconName, IconSize, InteractiveElement,
     IntoElement, Label, LabelCommon, Styled, Window, prelude::*,
 };
-use util::serde::default_true;
+use util::MergeFrom;
 use workspace::{AppState, ModalView, Workspace};
 
-#[derive(Deserialize)]
 pub struct SshSettings {
-    pub ssh_connections: Option<Vec<SshConnection>>,
-    pub wsl_connections: Option<Vec<WslConnection>>,
+    pub ssh_connections: Vec<SshConnection>,
+    pub wsl_connections: Vec<WslConnection>,
     /// Whether to read ~/.ssh/config for ssh connection sources.
-    #[serde(default = "default_true")]
     pub read_ssh_config: bool,
 }
 
 impl SshSettings {
     pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> + use<> {
-        self.ssh_connections.clone().into_iter().flatten()
+        self.ssh_connections.clone().into_iter()
     }
 
     pub fn wsl_connections(&self) -> impl Iterator<Item = WslConnection> + use<> {
-        self.wsl_connections.clone().into_iter().flatten()
+        self.wsl_connections.clone().into_iter()
     }
 
     pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {
@@ -80,66 +76,7 @@ impl SshSettings {
     }
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct SshConnection {
-    pub host: SharedString,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub username: Option<String>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub port: Option<u16>,
-    #[serde(skip_serializing_if = "Vec::is_empty")]
-    #[serde(default)]
-    pub args: Vec<String>,
-    #[serde(default)]
-    pub projects: BTreeSet<SshProject>,
-    /// Name to use for this server in UI.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub nickname: Option<String>,
-    // By default Zed will download the binary to the host directly.
-    // If this is set to true, Zed will download the binary to your local machine,
-    // and then upload it over the SSH connection. Useful if your SSH server has
-    // limited outbound internet access.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub upload_binary_over_ssh: Option<bool>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub port_forwards: Option<Vec<SshPortForwardOption>>,
-}
-
-impl From<SshConnection> for SshConnectionOptions {
-    fn from(val: SshConnection) -> Self {
-        SshConnectionOptions {
-            host: val.host.into(),
-            username: val.username,
-            port: val.port,
-            password: None,
-            args: Some(val.args),
-            nickname: val.nickname,
-            upload_binary_over_ssh: val.upload_binary_over_ssh.unwrap_or_default(),
-            port_forwards: val.port_forwards,
-        }
-    }
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
-pub struct WslConnection {
-    pub distro_name: SharedString,
-    #[serde(default)]
-    pub user: Option<String>,
-    #[serde(default)]
-    pub projects: BTreeSet<SshProject>,
-}
-
-impl From<WslConnection> for WslConnectionOptions {
-    fn from(val: WslConnection) -> Self {
-        WslConnectionOptions {
-            distro_name: val.distro_name.into(),
-            user: val.user,
-        }
-    }
-}
-
-#[derive(Clone, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(Clone, PartialEq)]
 pub enum Connection {
     Ssh(SshConnection),
     Wsl(WslConnection),
@@ -166,27 +103,26 @@ impl From<WslConnection> for Connection {
     }
 }
 
-#[derive(Clone, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema)]
-pub struct SshProject {
-    pub paths: Vec<String>,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
-pub struct RemoteSettingsContent {
-    pub ssh_connections: Option<Vec<SshConnection>>,
-    pub wsl_connections: Option<Vec<WslConnection>>,
-    pub read_ssh_config: Option<bool>,
-}
-
 impl Settings for SshSettings {
-    type FileContent = RemoteSettingsContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let remote = &content.remote;
+        Self {
+            ssh_connections: remote.ssh_connections.clone().unwrap_or_default(),
+            wsl_connections: remote.wsl_connections.clone().unwrap_or_default(),
+            read_ssh_config: remote.read_ssh_config.unwrap(),
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        if let Some(ssh_connections) = content.remote.ssh_connections.clone() {
+            self.ssh_connections.extend(ssh_connections)
+        }
+        if let Some(wsl_connections) = content.remote.wsl_connections.clone() {
+            self.wsl_connections.extend(wsl_connections)
+        }
+        self.read_ssh_config
+            .merge_from(&content.remote.read_ssh_config);
+    }
 }
 
 pub struct RemoteConnectionPrompt {

crates/recent_projects/src/remote_servers.rs πŸ”—

@@ -1,8 +1,7 @@
 use crate::{
     remote_connections::{
-        Connection, RemoteConnectionModal, RemoteConnectionPrompt, RemoteSettingsContent,
-        SshConnection, SshConnectionHeader, SshProject, SshSettings, connect, connect_over_ssh,
-        open_remote_project,
+        Connection, RemoteConnectionModal, RemoteConnectionPrompt, SshConnection,
+        SshConnectionHeader, SshSettings, connect, connect_over_ssh, open_remote_project,
     },
     ssh_config::parse_ssh_config_hosts,
 };
@@ -22,7 +21,10 @@ use remote::{
     RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
     remote_client::ConnectionIdentifier,
 };
-use settings::{Settings, SettingsStore, update_settings_file, watch_config_file};
+use settings::{
+    RemoteSettingsContent, Settings as _, SettingsStore, SshProject, update_settings_file,
+    watch_config_file,
+};
 use smol::stream::StreamExt as _;
 use std::{
     borrow::Cow,
@@ -233,14 +235,15 @@ impl ProjectPicker {
 
                     cx.update(|_, cx| {
                         let fs = app_state.fs.clone();
-                        update_settings_file::<SshSettings>(fs, cx, {
+                        update_settings_file(fs, cx, {
                             let paths = paths
                                 .iter()
                                 .map(|path| path.to_string_lossy().to_string())
                                 .collect();
-                            move |setting, _| match index {
+                            move |settings, _| match index {
                                 ServerIndex::Ssh(index) => {
-                                    if let Some(server) = setting
+                                    if let Some(server) = settings
+                                        .remote
                                         .ssh_connections
                                         .as_mut()
                                         .and_then(|connections| connections.get_mut(index.0))
@@ -249,7 +252,8 @@ impl ProjectPicker {
                                     };
                                 }
                                 ServerIndex::Wsl(index) => {
-                                    if let Some(server) = setting
+                                    if let Some(server) = settings
+                                        .remote
                                         .wsl_connections
                                         .as_mut()
                                         .and_then(|connections| connections.get_mut(index.0))
@@ -1317,7 +1321,7 @@ impl RemoteServerProjects {
         else {
             return;
         };
-        update_settings_file::<SshSettings>(fs, cx, move |setting, cx| f(setting, cx));
+        update_settings_file(fs, cx, move |setting, cx| f(&mut setting.remote, cx));
     }
 
     fn delete_ssh_server(&mut self, server: SshServerIndex, cx: &mut Context<Self>) {
@@ -1356,7 +1360,7 @@ impl RemoteServerProjects {
             setting
                 .wsl_connections
                 .get_or_insert(Default::default())
-                .push(crate::remote_connections::WslConnection {
+                .push(settings::WslConnection {
                     distro_name: SharedString::from(connection_options.distro_name),
                     user: connection_options.user,
                     projects: BTreeSet::new(),
@@ -1881,41 +1885,27 @@ impl RemoteServerProjects {
         let ssh_settings = SshSettings::get_global(cx);
         let mut should_rebuild = false;
 
-        let ssh_connections_changed =
-            ssh_settings
-                .ssh_connections
-                .as_ref()
-                .is_some_and(|connections| {
-                    state
-                        .servers
-                        .iter()
-                        .filter_map(|server| match server {
-                            RemoteEntry::Project {
-                                connection: Connection::Ssh(connection),
-                                ..
-                            } => Some(connection),
-                            _ => None,
-                        })
-                        .ne(connections.iter())
-                });
+        let ssh_connections_changed = ssh_settings.ssh_connections.iter().ne(state
+            .servers
+            .iter()
+            .filter_map(|server| match server {
+                RemoteEntry::Project {
+                    connection: Connection::Ssh(connection),
+                    ..
+                } => Some(connection),
+                _ => None,
+            }));
 
-        let wsl_connections_changed =
-            ssh_settings
-                .wsl_connections
-                .as_ref()
-                .is_some_and(|connections| {
-                    state
-                        .servers
-                        .iter()
-                        .filter_map(|server| match server {
-                            RemoteEntry::Project {
-                                connection: Connection::Wsl(connection),
-                                ..
-                            } => Some(connection),
-                            _ => None,
-                        })
-                        .ne(connections.iter())
-                });
+        let wsl_connections_changed = ssh_settings.wsl_connections.iter().ne(state
+            .servers
+            .iter()
+            .filter_map(|server| match server {
+                RemoteEntry::Project {
+                    connection: Connection::Wsl(connection),
+                    ..
+                } => Some(connection),
+                _ => None,
+            }));
 
         if ssh_connections_changed || wsl_connections_changed {
             should_rebuild = true;

crates/remote/Cargo.toml πŸ”—

@@ -32,9 +32,9 @@ paths.workspace = true
 prost.workspace = true
 release_channel.workspace = true
 rpc = { workspace = true, features = ["gpui"] }
-schemars.workspace =  true
 serde.workspace = true
 serde_json.workspace = true
+settings.workspace = true
 shlex.workspace = true
 smol.workspace = true
 tempfile.workspace = true

crates/remote/src/transport/ssh.rs πŸ”—

@@ -15,8 +15,7 @@ use itertools::Itertools;
 use parking_lot::Mutex;
 use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
 use rpc::proto::Envelope;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+pub use settings::SshPortForwardOption;
 use smol::{
     fs,
     process::{self, Child, Stdio},
@@ -54,14 +53,19 @@ pub struct SshConnectionOptions {
     pub upload_binary_over_ssh: bool,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
-pub struct SshPortForwardOption {
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub local_host: Option<String>,
-    pub local_port: u16,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub remote_host: Option<String>,
-    pub remote_port: u16,
+impl From<settings::SshConnection> for SshConnectionOptions {
+    fn from(val: settings::SshConnection) -> Self {
+        SshConnectionOptions {
+            host: val.host.into(),
+            username: val.username,
+            port: val.port,
+            password: None,
+            args: Some(val.args),
+            nickname: val.nickname,
+            upload_binary_over_ssh: val.upload_binary_over_ssh.unwrap_or_default(),
+            port_forwards: val.port_forwards,
+        }
+    }
 }
 
 #[derive(Clone)]

crates/remote/src/transport/wsl.rs πŸ”—

@@ -25,6 +25,15 @@ pub struct WslConnectionOptions {
     pub user: Option<String>,
 }
 
+impl From<settings::WslConnection> for WslConnectionOptions {
+    fn from(val: settings::WslConnection) -> Self {
+        WslConnectionOptions {
+            distro_name: val.distro_name.into(),
+            user: val.user,
+        }
+    }
+}
+
 pub(crate) struct WslRemoteConnection {
     remote_binary_path: Option<RemotePathBuf>,
     platform: RemotePlatform,

crates/repl/Cargo.toml πŸ”—

@@ -38,7 +38,6 @@ multi_buffer.workspace = true
 nbformat.workspace = true
 project.workspace = true
 runtimelib.workspace = true
-schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/repl/src/jupyter_settings.rs πŸ”—

@@ -1,10 +1,8 @@
-use std::collections::HashMap;
+use collections::HashMap;
 
 use editor::EditorSettings;
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::Settings;
 
 #[derive(Debug, Default)]
 pub struct JupyterSettings {
@@ -20,45 +18,20 @@ impl JupyterSettings {
     }
 }
 
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
-#[settings_key(key = "jupyter")]
-pub struct JupyterSettingsContent {
-    /// Default kernels to select for each language.
-    ///
-    /// Default: `{}`
-    pub kernel_selections: Option<HashMap<String, String>>,
-}
-
-impl Default for JupyterSettingsContent {
-    fn default() -> Self {
-        JupyterSettingsContent {
-            kernel_selections: Some(HashMap::new()),
+impl Settings for JupyterSettings {
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let jupyter = content.editor.jupyter.clone().unwrap();
+        Self {
+            kernel_selections: jupyter.kernel_selections.unwrap_or_default(),
         }
     }
-}
 
-impl Settings for JupyterSettings {
-    type FileContent = JupyterSettingsContent;
-
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _cx: &mut gpui::App,
-    ) -> anyhow::Result<Self>
-    where
-        Self: Sized,
-    {
-        let mut settings = JupyterSettings::default();
-
-        for value in sources.defaults_and_customizations() {
-            if let Some(source) = &value.kernel_selections {
-                for (k, v) in source {
-                    settings.kernel_selections.insert(k.clone(), v.clone());
-                }
-            }
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(jupyter) = content.editor.jupyter.as_ref() else {
+            return;
+        };
+        if let Some(kernel_selections) = jupyter.kernel_selections.clone() {
+            self.kernel_selections.extend(kernel_selections)
         }
-
-        Ok(settings)
     }
-
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
 }

crates/repl/src/repl_settings.rs πŸ”—

@@ -1,55 +1,38 @@
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::Settings;
+use util::MergeFrom;
 
 /// Settings for configuring REPL display and behavior.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(key = "repl")]
+#[derive(Clone, Debug)]
 pub struct ReplSettings {
     /// Maximum number of lines to keep in REPL's scrollback buffer.
     /// Clamped with [4, 256] range.
     ///
     /// Default: 32
-    #[serde(default = "default_max_lines")]
     pub max_lines: usize,
     /// Maximum number of columns to keep in REPL's scrollback buffer.
     /// Clamped with [20, 512] range.
     ///
     /// Default: 128
-    #[serde(default = "default_max_columns")]
     pub max_columns: usize,
 }
 
 impl Settings for ReplSettings {
-    type FileContent = Self;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let repl = content.repl.as_ref().unwrap();
 
-    fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> anyhow::Result<Self> {
-        let mut settings: ReplSettings = sources.json_merge()?;
-        settings.max_columns = settings.max_columns.clamp(20, 512);
-        settings.max_lines = settings.max_lines.clamp(4, 256);
-        Ok(settings)
+        Self {
+            max_lines: repl.max_lines.unwrap(),
+            max_columns: repl.max_columns.unwrap(),
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
-}
-
-const DEFAULT_NUM_LINES: usize = 32;
-const DEFAULT_NUM_COLUMNS: usize = 128;
-
-fn default_max_lines() -> usize {
-    DEFAULT_NUM_LINES
-}
-
-fn default_max_columns() -> usize {
-    DEFAULT_NUM_COLUMNS
-}
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(repl) = content.repl.as_ref() else {
+            return;
+        };
 
-impl Default for ReplSettings {
-    fn default() -> Self {
-        ReplSettings {
-            max_lines: DEFAULT_NUM_LINES,
-            max_columns: DEFAULT_NUM_COLUMNS,
-        }
+        self.max_columns.merge_from(&repl.max_columns);
+        self.max_lines.merge_from(&repl.max_lines);
     }
 }

crates/search/src/buffer_search.rs πŸ”—

@@ -1480,7 +1480,7 @@ mod tests {
     use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
     use language::{Buffer, Point};
     use project::Project;
-    use settings::SettingsStore;
+    use settings::{SearchSettingsContent, SettingsStore};
     use smol::stream::StreamExt as _;
     use unindent::Unindent as _;
 
@@ -2866,8 +2866,14 @@ mod tests {
     fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) {
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<EditorSettings>(cx, |settings| {
-                    settings.search = Some(search_settings);
+                store.update_user_settings(cx, |settings| {
+                    settings.editor.search = Some(SearchSettingsContent {
+                        button: Some(search_settings.button),
+                        whole_word: Some(search_settings.whole_word),
+                        case_sensitive: Some(search_settings.case_sensitive),
+                        include_ignored: Some(search_settings.include_ignored),
+                        regex: Some(search_settings.regex),
+                    });
                 });
             });
         });

crates/settings/Cargo.toml πŸ”—

@@ -32,7 +32,9 @@ serde.workspace = true
 serde_json.workspace = true
 settings_ui_macros.workspace = true
 serde_json_lenient.workspace = true
+serde_repr.workspace = true
 serde_path_to_error.workspace = true
+serde_with.workspace = true
 smallvec.workspace = true
 tree-sitter-json.workspace = true
 tree-sitter.workspace = true

crates/settings/src/base_keymap_setting.rs πŸ”—

@@ -1,9 +1,14 @@
 use std::fmt::{Display, Formatter};
 
-use crate::{self as settings};
+use crate::{
+    self as settings,
+    settings_content::{self, BaseKeymapContent, SettingsContent},
+};
+use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, VsCodeSettings};
+use serde_with::skip_serializing_none;
+use settings::{Settings, VsCodeSettings};
 use settings_ui_macros::{SettingsKey, SettingsUi};
 
 /// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
@@ -24,6 +29,35 @@ pub enum BaseKeymap {
     None,
 }
 
+impl From<BaseKeymapContent> for BaseKeymap {
+    fn from(value: BaseKeymapContent) -> Self {
+        match value {
+            BaseKeymapContent::VSCode => Self::VSCode,
+            BaseKeymapContent::JetBrains => Self::JetBrains,
+            BaseKeymapContent::SublimeText => Self::SublimeText,
+            BaseKeymapContent::Atom => Self::Atom,
+            BaseKeymapContent::TextMate => Self::TextMate,
+            BaseKeymapContent::Emacs => Self::Emacs,
+            BaseKeymapContent::Cursor => Self::Cursor,
+            BaseKeymapContent::None => Self::None,
+        }
+    }
+}
+impl Into<BaseKeymapContent> for BaseKeymap {
+    fn into(self) -> BaseKeymapContent {
+        match self {
+            BaseKeymap::VSCode => BaseKeymapContent::VSCode,
+            BaseKeymap::JetBrains => BaseKeymapContent::JetBrains,
+            BaseKeymap::SublimeText => BaseKeymapContent::SublimeText,
+            BaseKeymap::Atom => BaseKeymapContent::Atom,
+            BaseKeymap::TextMate => BaseKeymapContent::TextMate,
+            BaseKeymap::Emacs => BaseKeymapContent::Emacs,
+            BaseKeymap::Cursor => BaseKeymapContent::Cursor,
+            BaseKeymap::None => BaseKeymapContent::None,
+        }
+    }
+}
+
 impl Display for BaseKeymap {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
         match self {
@@ -115,30 +149,23 @@ impl BaseKeymap {
 )]
 // extracted so that it can be an option, and still work with derive(SettingsUi)
 #[settings_key(None)]
+#[skip_serializing_none]
 pub struct BaseKeymapSetting {
     pub base_keymap: Option<BaseKeymap>,
 }
 
 impl Settings for BaseKeymap {
-    type FileContent = BaseKeymapSetting;
+    fn from_defaults(s: &crate::settings_content::SettingsContent, _cx: &mut App) -> Self {
+        s.base_keymap.unwrap().into()
+    }
 
-    fn load(
-        sources: SettingsSources<Self::FileContent>,
-        _: &mut gpui::App,
-    ) -> anyhow::Result<Self> {
-        if let Some(Some(user_value)) = sources.user.map(|setting| setting.base_keymap) {
-            return Ok(user_value);
-        }
-        if let Some(Some(server_value)) = sources.server.map(|setting| setting.base_keymap) {
-            return Ok(server_value);
-        }
-        sources
-            .default
-            .base_keymap
-            .ok_or_else(Self::missing_default)
+    fn refine(&mut self, s: &settings_content::SettingsContent, _cx: &mut App) {
+        if let Some(base_keymap) = s.base_keymap {
+            *self = base_keymap.into();
+        };
     }
 
-    fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut Self::FileContent) {
-        current.base_keymap = Some(BaseKeymap::VSCode);
+    fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut SettingsContent) {
+        current.base_keymap = Some(BaseKeymapContent::VSCode);
     }
 }

crates/settings/src/editable_setting_control.rs πŸ”—

@@ -1,16 +1,13 @@
 use fs::Fs;
 use gpui::{App, RenderOnce, SharedString};
 
-use crate::{Settings, update_settings_file};
+use crate::{settings_content::SettingsContent, update_settings_file};
 
 /// A UI control that can be used to edit a setting.
 pub trait EditableSettingControl: RenderOnce {
     /// The type of the setting value.
     type Value: Send;
 
-    /// The settings type to which this setting belongs.
-    type Settings: Settings;
-
     /// Returns the name of this setting.
     fn name(&self) -> SharedString;
 
@@ -20,17 +17,13 @@ pub trait EditableSettingControl: RenderOnce {
     /// Applies the given setting file to the settings file contents.
     ///
     /// This will be called when writing the setting value back to the settings file.
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        cx: &App,
-    );
+    fn apply(settings: &mut SettingsContent, value: Self::Value, cx: &App);
 
     /// Writes the given setting value to the settings files.
     fn write(value: Self::Value, cx: &App) {
         let fs = <dyn Fs>::global(cx);
 
-        update_settings_file::<Self::Settings>(fs, cx, move |settings, cx| {
+        update_settings_file(fs, cx, move |settings, cx| {
             Self::apply(settings, value, cx);
         });
     }

crates/settings/src/settings.rs πŸ”—

@@ -1,12 +1,15 @@
 mod base_keymap_setting;
 mod editable_setting_control;
 mod keymap_file;
+mod settings_content;
 mod settings_file;
 mod settings_json;
 mod settings_store;
 mod settings_ui_core;
 mod vscode_import;
 
+pub use settings_content::*;
+
 use gpui::{App, Global};
 use rust_embed::RustEmbed;
 use std::{borrow::Cow, fmt, str};
@@ -21,8 +24,7 @@ pub use keymap_file::{
 pub use settings_file::*;
 pub use settings_json::*;
 pub use settings_store::{
-    InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation,
-    SettingsSources, SettingsStore,
+    InvalidSettingsError, LocalSettingsKind, Settings, SettingsKey, SettingsLocation, SettingsStore,
 };
 pub use settings_ui_core::*;
 // Re-export the derive macro
@@ -75,10 +77,7 @@ impl fmt::Display for WorktreeId {
 pub struct SettingsAssets;
 
 pub fn init(cx: &mut App) {
-    let mut settings = SettingsStore::new(cx);
-    settings
-        .set_default_settings(&default_settings(), cx)
-        .unwrap();
+    let settings = SettingsStore::new(cx, &default_settings());
     cx.set_global(settings);
     BaseKeymap::register(cx);
     SettingsStore::observe_active_settings_profile_name(cx).detach();

crates/settings/src/settings_content.rs πŸ”—

@@ -0,0 +1,818 @@
+mod agent;
+mod editor;
+mod language;
+mod language_model;
+mod project;
+mod terminal;
+mod theme;
+mod workspace;
+
+pub use agent::*;
+pub use editor::*;
+pub use language::*;
+pub use language_model::*;
+pub use project::*;
+pub use terminal::*;
+pub use theme::*;
+pub use workspace::*;
+
+use collections::HashMap;
+use gpui::{App, SharedString};
+use release_channel::ReleaseChannel;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use std::collections::BTreeSet;
+use std::env;
+use std::sync::Arc;
+pub use util::serde::default_true;
+
+use crate::ActiveSettingsProfileName;
+
+#[skip_serializing_none]
+#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct SettingsContent {
+    #[serde(flatten)]
+    pub project: ProjectSettingsContent,
+
+    #[serde(flatten)]
+    pub theme: Box<ThemeSettingsContent>,
+
+    #[serde(flatten)]
+    pub extension: ExtensionSettingsContent,
+
+    #[serde(flatten)]
+    pub workspace: WorkspaceSettingsContent,
+
+    #[serde(flatten)]
+    pub editor: EditorSettingsContent,
+
+    #[serde(flatten)]
+    pub remote: RemoteSettingsContent,
+
+    /// Settings related to the file finder.
+    pub file_finder: Option<FileFinderSettingsContent>,
+
+    pub git_panel: Option<GitPanelSettingsContent>,
+
+    pub tabs: Option<ItemSettingsContent>,
+    pub tab_bar: Option<TabBarSettingsContent>,
+
+    pub preview_tabs: Option<PreviewTabsSettingsContent>,
+
+    pub agent: Option<AgentSettingsContent>,
+    pub agent_servers: Option<AllAgentServersSettings>,
+
+    /// Configuration of audio in Zed.
+    pub audio: Option<AudioSettingsContent>,
+
+    /// Whether or not to automatically check for updates.
+    ///
+    /// Default: true
+    pub auto_update: Option<bool>,
+
+    /// This base keymap settings adjusts the default keybindings in Zed to be similar
+    /// to other common code editors. By default, Zed's keymap closely follows VSCode's
+    /// keymap, with minor adjustments, this corresponds to the "VSCode" setting.
+    ///
+    /// Default: VSCode
+    pub base_keymap: Option<BaseKeymapContent>,
+
+    /// Configuration for the collab panel visual settings.
+    pub collaboration_panel: Option<PanelSettingsContent>,
+
+    pub debugger: Option<DebuggerSettingsContent>,
+
+    /// Configuration for Diagnostics-related features.
+    pub diagnostics: Option<DiagnosticsSettingsContent>,
+
+    /// Configuration for Git-related features
+    pub git: Option<GitSettings>,
+
+    /// Common language server settings.
+    pub global_lsp_settings: Option<GlobalLspSettingsContent>,
+
+    /// The settings for the image viewer.
+    pub image_viewer: Option<ImageViewerSettingsContent>,
+
+    pub repl: Option<ReplSettingsContent>,
+
+    /// Whether or not to enable Helix mode.
+    ///
+    /// Default: false
+    pub helix_mode: Option<bool>,
+
+    pub journal: Option<JournalSettingsContent>,
+
+    /// A map of log scopes to the desired log level.
+    /// Useful for filtering out noisy logs or enabling more verbose logging.
+    ///
+    /// Example: {"log": {"client": "warn"}}
+    pub log: Option<HashMap<String, String>>,
+
+    pub line_indicator_format: Option<LineIndicatorFormat>,
+
+    pub language_models: Option<AllLanguageModelSettingsContent>,
+
+    pub outline_panel: Option<OutlinePanelSettingsContent>,
+
+    pub project_panel: Option<ProjectPanelSettingsContent>,
+
+    /// Configuration for the Message Editor
+    pub message_editor: Option<MessageEditorSettings>,
+
+    /// Configuration for Node-related features
+    pub node: Option<NodeBinarySettings>,
+
+    /// Configuration for the Notification Panel
+    pub notification_panel: Option<NotificationPanelSettingsContent>,
+
+    pub proxy: Option<String>,
+
+    /// The URL of the Zed server to connect to.
+    pub server_url: Option<String>,
+
+    /// Configuration for session-related features
+    pub session: Option<SessionSettingsContent>,
+    /// Control what info is collected by Zed.
+    pub telemetry: Option<TelemetrySettingsContent>,
+
+    /// Configuration of the terminal in Zed.
+    pub terminal: Option<TerminalSettingsContent>,
+
+    pub title_bar: Option<TitleBarSettingsContent>,
+
+    /// Whether or not to enable Vim mode.
+    ///
+    /// Default: false
+    pub vim_mode: Option<bool>,
+
+    // Settings related to calls in Zed
+    pub calls: Option<CallSettingsContent>,
+
+    /// Whether to disable all AI features in Zed.
+    ///
+    /// Default: false
+    pub disable_ai: Option<bool>,
+
+    /// Settings related to Vim mode in Zed.
+    pub vim: Option<VimSettingsContent>,
+}
+
+impl SettingsContent {
+    pub fn languages_mut(&mut self) -> &mut HashMap<SharedString, LanguageSettingsContent> {
+        &mut self.project.all_languages.languages.0
+    }
+}
+
+#[skip_serializing_none]
+#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ServerSettingsContent {
+    #[serde(flatten)]
+    pub project: ProjectSettingsContent,
+}
+
+#[skip_serializing_none]
+#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct UserSettingsContent {
+    #[serde(flatten)]
+    pub content: Box<SettingsContent>,
+
+    pub dev: Option<Box<SettingsContent>>,
+    pub nightly: Option<Box<SettingsContent>>,
+    pub preview: Option<Box<SettingsContent>>,
+    pub stable: Option<Box<SettingsContent>>,
+
+    pub macos: Option<Box<SettingsContent>>,
+    pub windows: Option<Box<SettingsContent>>,
+    pub linux: Option<Box<SettingsContent>>,
+
+    #[serde(default)]
+    pub profiles: HashMap<String, SettingsContent>,
+}
+
+pub struct ExtensionsSettingsContent {
+    pub all_languages: AllLanguageSettingsContent,
+}
+
+impl UserSettingsContent {
+    pub fn for_release_channel(&self) -> Option<&SettingsContent> {
+        match *release_channel::RELEASE_CHANNEL {
+            ReleaseChannel::Dev => self.dev.as_deref(),
+            ReleaseChannel::Nightly => self.nightly.as_deref(),
+            ReleaseChannel::Preview => self.preview.as_deref(),
+            ReleaseChannel::Stable => self.stable.as_deref(),
+        }
+    }
+
+    pub fn for_os(&self) -> Option<&SettingsContent> {
+        match env::consts::OS {
+            "macos" => self.macos.as_deref(),
+            "linux" => self.linux.as_deref(),
+            "windows" => self.windows.as_deref(),
+            _ => None,
+        }
+    }
+
+    pub fn for_profile(&self, cx: &App) -> Option<&SettingsContent> {
+        let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() else {
+            return None;
+        };
+        self.profiles.get(&active_profile.0)
+    }
+}
+
+/// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
+///
+/// Default: VSCode
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+pub enum BaseKeymapContent {
+    #[default]
+    VSCode,
+    JetBrains,
+    SublimeText,
+    Atom,
+    TextMate,
+    Emacs,
+    Cursor,
+    None,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct TitleBarSettingsContent {
+    /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen".
+    ///
+    /// Default: "always"
+    pub show: Option<TitleBarVisibility>,
+    /// Whether to show the branch icon beside branch switcher in the title bar.
+    ///
+    /// Default: false
+    pub show_branch_icon: Option<bool>,
+    /// Whether to show onboarding banners in the title bar.
+    ///
+    /// Default: true
+    pub show_onboarding_banner: Option<bool>,
+    /// Whether to show user avatar in the title bar.
+    ///
+    /// Default: true
+    pub show_user_picture: Option<bool>,
+    /// Whether to show the branch name button in the titlebar.
+    ///
+    /// Default: true
+    pub show_branch_name: Option<bool>,
+    /// Whether to show the project host and name in the titlebar.
+    ///
+    /// Default: true
+    pub show_project_items: Option<bool>,
+    /// Whether to show the sign in button in the title bar.
+    ///
+    /// Default: true
+    pub show_sign_in: Option<bool>,
+    /// Whether to show the menus in the title bar.
+    ///
+    /// Default: false
+    pub show_menus: Option<bool>,
+}
+
+#[derive(Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum TitleBarVisibility {
+    Always,
+    Never,
+    HideInFullScreen,
+}
+
+/// Configuration of audio in Zed.
+#[skip_serializing_none]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct AudioSettingsContent {
+    /// Opt into the new audio system.
+    #[serde(rename = "experimental.rodio_audio", default)]
+    pub rodio_audio: Option<bool>,
+    /// Requires 'rodio_audio: true'
+    ///
+    /// Use the new audio systems automatic gain control for your microphone.
+    /// This affects how loud you sound to others.
+    #[serde(rename = "experimental.control_input_volume", default)]
+    pub control_input_volume: Option<bool>,
+    /// Requires 'rodio_audio: true'
+    ///
+    /// Use the new audio systems automatic gain control on everyone in the
+    /// call. This makes call members who are too quite louder and those who are
+    /// too loud quieter. This only affects how things sound for you.
+    #[serde(rename = "experimental.control_output_volume", default)]
+    pub control_output_volume: Option<bool>,
+}
+
+/// Control what info is collected by Zed.
+#[skip_serializing_none]
+#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct TelemetrySettingsContent {
+    /// Send debug info like crash reports.
+    ///
+    /// Default: true
+    pub diagnostics: Option<bool>,
+    /// Send anonymized usage data like what languages you're using Zed with.
+    ///
+    /// Default: true
+    pub metrics: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone)]
+pub struct DebuggerSettingsContent {
+    /// Determines the stepping granularity.
+    ///
+    /// Default: line
+    pub stepping_granularity: Option<SteppingGranularity>,
+    /// Whether the breakpoints should be reused across Zed sessions.
+    ///
+    /// Default: true
+    pub save_breakpoints: Option<bool>,
+    /// Whether to show the debug button in the status bar.
+    ///
+    /// Default: true
+    pub button: Option<bool>,
+    /// Time in milliseconds until timeout error when connecting to a TCP debug adapter
+    ///
+    /// Default: 2000ms
+    pub timeout: Option<u64>,
+    /// Whether to log messages between active debug adapters and Zed
+    ///
+    /// Default: true
+    pub log_dap_communications: Option<bool>,
+    /// Whether to format dap messages in when adding them to debug adapter logger
+    ///
+    /// Default: true
+    pub format_dap_log_messages: Option<bool>,
+    /// The dock position of the debug panel
+    ///
+    /// Default: Bottom
+    pub dock: Option<DockPosition>,
+}
+
+/// The granularity of one 'step' in the stepping requests `next`, `stepIn`, `stepOut`, and `stepBack`.
+#[derive(PartialEq, Eq, Debug, Hash, Clone, Copy, Deserialize, Serialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SteppingGranularity {
+    /// The step should allow the program to run until the current statement has finished executing.
+    /// The meaning of a statement is determined by the adapter and it may be considered equivalent to a line.
+    /// For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'.
+    Statement,
+    /// The step should allow the program to run until the current source line has executed.
+    Line,
+    /// The step should allow one instruction to execute (e.g. one x86 instruction).
+    Instruction,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum DockPosition {
+    Left,
+    Bottom,
+    Right,
+}
+
+/// Settings for slash commands.
+#[skip_serializing_none]
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, PartialEq, Eq)]
+pub struct SlashCommandSettings {
+    /// Settings for the `/cargo-workspace` slash command.
+    pub cargo_workspace: Option<CargoWorkspaceCommandSettings>,
+}
+
+/// Settings for the `/cargo-workspace` slash command.
+#[skip_serializing_none]
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, PartialEq, Eq)]
+pub struct CargoWorkspaceCommandSettings {
+    /// Whether `/cargo-workspace` is enabled.
+    pub enabled: Option<bool>,
+}
+
+/// Configuration of voice calls in Zed.
+#[skip_serializing_none]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct CallSettingsContent {
+    /// Whether the microphone should be muted when joining a channel or a call.
+    ///
+    /// Default: false
+    pub mute_on_join: Option<bool>,
+
+    /// Whether your current project should be shared when joining an empty channel.
+    ///
+    /// Default: false
+    pub share_on_join: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Deserialize, Serialize, PartialEq, Debug, Default, Clone, JsonSchema)]
+pub struct ExtensionSettingsContent {
+    /// The extensions that should be automatically installed by Zed.
+    ///
+    /// This is used to make functionality provided by extensions (e.g., language support)
+    /// available out-of-the-box.
+    ///
+    /// Default: { "html": true }
+    #[serde(default)]
+    pub auto_install_extensions: HashMap<Arc<str>, bool>,
+    #[serde(default)]
+    pub auto_update_extensions: HashMap<Arc<str>, bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct GitPanelSettingsContent {
+    /// Whether to show the panel button in the status bar.
+    ///
+    /// Default: true
+    pub button: Option<bool>,
+    /// Where to dock the panel.
+    ///
+    /// Default: left
+    pub dock: Option<DockPosition>,
+    /// Default width of the panel in pixels.
+    ///
+    /// Default: 360
+    pub default_width: Option<f32>,
+    /// How entry statuses are displayed.
+    ///
+    /// Default: icon
+    pub status_style: Option<StatusStyle>,
+    /// How and when the scrollbar should be displayed.
+    ///
+    /// Default: inherits editor scrollbar settings
+    pub scrollbar: Option<ScrollbarSettings>,
+
+    /// What the default branch name should be when
+    /// `init.defaultBranch` is not set in git
+    ///
+    /// Default: main
+    pub fallback_branch_name: Option<String>,
+
+    /// Whether to sort entries in the panel by path
+    /// or by status (the default).
+    ///
+    /// Default: false
+    pub sort_by_path: Option<bool>,
+
+    /// Whether to collapse untracked files in the diff panel.
+    ///
+    /// Default: false
+    pub collapse_untracked_diff: Option<bool>,
+}
+
+#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum StatusStyle {
+    #[default]
+    Icon,
+    LabelColor,
+}
+
+#[skip_serializing_none]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ScrollbarSettings {
+    pub show: Option<ShowScrollbar>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+pub struct NotificationPanelSettingsContent {
+    /// Whether to show the panel button in the status bar.
+    ///
+    /// Default: true
+    pub button: Option<bool>,
+    /// Where to dock the panel.
+    ///
+    /// Default: right
+    pub dock: Option<DockPosition>,
+    /// Default width of the panel in pixels.
+    ///
+    /// Default: 300
+    pub default_width: Option<f32>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+pub struct PanelSettingsContent {
+    /// Whether to show the panel button in the status bar.
+    ///
+    /// Default: true
+    pub button: Option<bool>,
+    /// Where to dock the panel.
+    ///
+    /// Default: left
+    pub dock: Option<DockPosition>,
+    /// Default width of the panel in pixels.
+    ///
+    /// Default: 240
+    pub default_width: Option<f32>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+pub struct MessageEditorSettings {
+    /// Whether to automatically replace emoji shortcodes with emoji characters.
+    /// For example: typing `:wave:` gets replaced with `πŸ‘‹`.
+    ///
+    /// Default: false
+    pub auto_replace_emoji_shortcode: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+pub struct FileFinderSettingsContent {
+    /// Whether to show file icons in the file finder.
+    ///
+    /// Default: true
+    pub file_icons: Option<bool>,
+    /// Determines how much space the file finder can take up in relation to the available window width.
+    ///
+    /// Default: small
+    pub modal_max_width: Option<FileFinderWidthContent>,
+    /// Determines whether the file finder should skip focus for the active file in search results.
+    ///
+    /// Default: true
+    pub skip_focus_for_active_in_search: Option<bool>,
+    /// Determines whether to show the git status in the file finder
+    ///
+    /// Default: true
+    pub git_status: Option<bool>,
+    /// Whether to use gitignored files when searching.
+    /// Only the file Zed had indexed will be used, not necessary all the gitignored files.
+    ///
+    /// Can accept 3 values:
+    /// * `Some(true)`: Use all gitignored files
+    /// * `Some(false)`: Use only the files Zed had indexed
+    /// * `None`: Be smart and search for ignored when called from a gitignored worktree
+    ///
+    /// Default: None
+    /// todo() -> Change this type to an enum
+    pub include_ignored: Option<Option<bool>>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum FileFinderWidthContent {
+    #[default]
+    Small,
+    Medium,
+    Large,
+    XLarge,
+    Full,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug, JsonSchema)]
+pub struct VimSettingsContent {
+    pub default_mode: Option<ModeContent>,
+    pub toggle_relative_line_numbers: Option<bool>,
+    pub use_system_clipboard: Option<UseSystemClipboard>,
+    pub use_smartcase_find: Option<bool>,
+    pub custom_digraphs: Option<HashMap<String, Arc<str>>>,
+    pub highlight_on_yank_duration: Option<u64>,
+    pub cursor_shape: Option<CursorShapeSettings>,
+}
+
+#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum ModeContent {
+    #[default]
+    Normal,
+    Insert,
+    HelixNormal,
+}
+
+/// Controls when to use system clipboard.
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum UseSystemClipboard {
+    /// Don't use system clipboard.
+    Never,
+    /// Use system clipboard.
+    Always,
+    /// Use system clipboard for yank operations.
+    OnYank,
+}
+
+/// The settings for cursor shape.
+#[skip_serializing_none]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+pub struct CursorShapeSettings {
+    /// Cursor shape for the normal mode.
+    ///
+    /// Default: block
+    pub normal: Option<CursorShape>,
+    /// Cursor shape for the replace mode.
+    ///
+    /// Default: underline
+    pub replace: Option<CursorShape>,
+    /// Cursor shape for the visual mode.
+    ///
+    /// Default: block
+    pub visual: Option<CursorShape>,
+    /// Cursor shape for the insert mode.
+    ///
+    /// The default value follows the primary cursor_shape.
+    pub insert: Option<CursorShape>,
+}
+
+/// Settings specific to journaling
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct JournalSettingsContent {
+    /// The path of the directory where journal entries are stored.
+    ///
+    /// Default: `~`
+    pub path: Option<String>,
+    /// What format to display the hours in.
+    ///
+    /// Default: hour12
+    pub hour_format: Option<HourFormat>,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum HourFormat {
+    #[default]
+    Hour12,
+    Hour24,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+pub struct OutlinePanelSettingsContent {
+    /// Whether to show the outline panel button in the status bar.
+    ///
+    /// Default: true
+    pub button: Option<bool>,
+    /// Customize default width (in pixels) taken by outline panel
+    ///
+    /// Default: 240
+    pub default_width: Option<f32>,
+    /// The position of outline panel
+    ///
+    /// Default: left
+    pub dock: Option<DockSide>,
+    /// Whether to show file icons in the outline panel.
+    ///
+    /// Default: true
+    pub file_icons: Option<bool>,
+    /// Whether to show folder icons or chevrons for directories in the outline panel.
+    ///
+    /// Default: true
+    pub folder_icons: Option<bool>,
+    /// Whether to show the git status in the outline panel.
+    ///
+    /// Default: true
+    pub git_status: Option<bool>,
+    /// Amount of indentation (in pixels) for nested items.
+    ///
+    /// Default: 20
+    pub indent_size: Option<f32>,
+    /// Whether to reveal it in the outline panel automatically,
+    /// when a corresponding project entry becomes active.
+    /// Gitignored entries are never auto revealed.
+    ///
+    /// Default: true
+    pub auto_reveal_entries: Option<bool>,
+    /// Whether to fold directories automatically
+    /// when directory has only one directory inside.
+    ///
+    /// Default: true
+    pub auto_fold_dirs: Option<bool>,
+    /// Settings related to indent guides in the outline panel.
+    pub indent_guides: Option<IndentGuidesSettingsContent>,
+    /// Scrollbar-related settings
+    pub scrollbar: Option<ScrollbarSettingsContent>,
+    /// Default depth to expand outline items in the current file.
+    /// The default depth to which outline entries are expanded on reveal.
+    /// - Set to 0 to collapse all items that have children
+    /// - Set to 1 or higher to collapse items at that depth or deeper
+    ///
+    /// Default: 100
+    pub expand_outlines_with_depth: Option<usize>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum DockSide {
+    Left,
+    Right,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowIndentGuides {
+    Always,
+    Never,
+}
+
+#[skip_serializing_none]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct IndentGuidesSettingsContent {
+    /// When to show the scrollbar in the outline panel.
+    pub show: Option<ShowIndentGuides>,
+}
+
+#[derive(Clone, Copy, Default, PartialEq, Debug, JsonSchema, Deserialize, Serialize)]
+#[serde(rename_all = "snake_case")]
+pub enum LineIndicatorFormat {
+    Short,
+    #[default]
+    Long,
+}
+
+/// The settings for the image viewer.
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
+pub struct ImageViewerSettingsContent {
+    /// The unit to use for displaying image file sizes.
+    ///
+    /// Default: "binary"
+    pub unit: Option<ImageFileSizeUnit>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum ImageFileSizeUnit {
+    /// Displays file size in binary units (e.g., KiB, MiB).
+    #[default]
+    Binary,
+    /// Displays file size in decimal units (e.g., KB, MB).
+    Decimal,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct RemoteSettingsContent {
+    pub ssh_connections: Option<Vec<SshConnection>>,
+    pub wsl_connections: Option<Vec<WslConnection>>,
+    pub read_ssh_config: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct SshConnection {
+    pub host: SharedString,
+    pub username: Option<String>,
+    pub port: Option<u16>,
+    #[serde(default)]
+    pub args: Vec<String>,
+    #[serde(default)]
+    pub projects: collections::BTreeSet<SshProject>,
+    /// Name to use for this server in UI.
+    pub nickname: Option<String>,
+    // By default Zed will download the binary to the host directly.
+    // If this is set to true, Zed will download the binary to your local machine,
+    // and then upload it over the SSH connection. Useful if your SSH server has
+    // limited outbound internet access.
+    pub upload_binary_over_ssh: Option<bool>,
+
+    pub port_forwards: Option<Vec<SshPortForwardOption>>,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema, Debug)]
+pub struct WslConnection {
+    pub distro_name: SharedString,
+    pub user: Option<String>,
+    #[serde(default)]
+    pub projects: BTreeSet<SshProject>,
+}
+
+#[skip_serializing_none]
+#[derive(
+    Clone, Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema,
+)]
+pub struct SshProject {
+    pub paths: Vec<String>,
+}
+
+#[skip_serializing_none]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
+pub struct SshPortForwardOption {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub local_host: Option<String>,
+    pub local_port: u16,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub remote_host: Option<String>,
+    pub remote_port: u16,
+}
+
+/// Settings for configuring REPL display and behavior.
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct ReplSettingsContent {
+    /// Maximum number of lines to keep in REPL's scrollback buffer.
+    /// Clamped with [4, 256] range.
+    ///
+    /// Default: 32
+    pub max_lines: Option<usize>,
+    /// Maximum number of columns to keep in REPL's scrollback buffer.
+    /// Clamped with [20, 512] range.
+    ///
+    /// Default: 128
+    pub max_columns: Option<usize>,
+}

crates/settings/src/settings_content/agent.rs πŸ”—

@@ -0,0 +1,336 @@
+use collections::{HashMap, IndexMap};
+use gpui::SharedString;
+use schemars::{JsonSchema, json_schema};
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use std::{borrow::Cow, path::PathBuf, sync::Arc};
+
+use crate::DockPosition;
+
+#[skip_serializing_none]
+#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, Debug, Default)]
+pub struct AgentSettingsContent {
+    /// Whether the Agent is enabled.
+    ///
+    /// Default: true
+    pub enabled: Option<bool>,
+    /// Whether to show the agent panel button in the status bar.
+    ///
+    /// Default: true
+    pub button: Option<bool>,
+    /// Where to dock the agent panel.
+    ///
+    /// Default: right
+    pub dock: Option<DockPosition>,
+    /// Default width in pixels when the agent panel is docked to the left or right.
+    ///
+    /// Default: 640
+    pub default_width: Option<f32>,
+    /// Default height in pixels when the agent panel is docked to the bottom.
+    ///
+    /// Default: 320
+    pub default_height: Option<f32>,
+    /// The default model to use when creating new chats and for other features when a specific model is not specified.
+    pub default_model: Option<LanguageModelSelection>,
+    /// Model to use for the inline assistant. Defaults to default_model when not specified.
+    pub inline_assistant_model: Option<LanguageModelSelection>,
+    /// Model to use for generating git commit messages. Defaults to default_model when not specified.
+    pub commit_message_model: Option<LanguageModelSelection>,
+    /// Model to use for generating thread summaries. Defaults to default_model when not specified.
+    pub thread_summary_model: Option<LanguageModelSelection>,
+    /// Additional models with which to generate alternatives when performing inline assists.
+    pub inline_alternatives: Option<Vec<LanguageModelSelection>>,
+    /// The default profile to use in the Agent.
+    ///
+    /// Default: write
+    pub default_profile: Option<Arc<str>>,
+    /// Which view type to show by default in the agent panel.
+    ///
+    /// Default: "thread"
+    pub default_view: Option<DefaultAgentView>,
+    /// The available agent profiles.
+    pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
+    /// Whenever a tool action would normally wait for your confirmation
+    /// that you allow it, always choose to allow it.
+    ///
+    /// This setting has no effect on external agents that support permission modes, such as Claude Code.
+    ///
+    /// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code.
+    ///
+    /// Default: false
+    pub always_allow_tool_actions: Option<bool>,
+    /// Where to show a popup notification when the agent is waiting for user input.
+    ///
+    /// Default: "primary_screen"
+    pub notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
+    /// Whether to play a sound when the agent has either completed its response, or needs user input.
+    ///
+    /// Default: false
+    pub play_sound_when_agent_done: Option<bool>,
+    /// Whether to stream edits from the agent as they are received.
+    ///
+    /// Default: false
+    pub stream_edits: Option<bool>,
+    /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
+    ///
+    /// Default: true
+    pub single_file_review: Option<bool>,
+    /// Additional parameters for language model requests. When making a request
+    /// to a model, parameters will be taken from the last entry in this list
+    /// that matches the model's provider and name. In each entry, both provider
+    /// and model are optional, so that you can specify parameters for either
+    /// one.
+    ///
+    /// Default: []
+    #[serde(default)]
+    pub model_parameters: Vec<LanguageModelParameters>,
+    /// What completion mode to enable for new threads
+    ///
+    /// Default: normal
+    pub preferred_completion_mode: Option<CompletionMode>,
+    /// Whether to show thumb buttons for feedback in the agent panel.
+    ///
+    /// Default: true
+    pub enable_feedback: Option<bool>,
+    /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
+    ///
+    /// Default: true
+    pub expand_edit_card: Option<bool>,
+    /// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
+    ///
+    /// Default: true
+    pub expand_terminal_card: Option<bool>,
+    /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
+    ///
+    /// Default: false
+    pub use_modifier_to_send: Option<bool>,
+    /// Minimum number of lines of height the agent message editor should have.
+    ///
+    /// Default: 4
+    pub message_editor_min_lines: Option<usize>,
+}
+
+impl AgentSettingsContent {
+    pub fn set_dock(&mut self, dock: DockPosition) {
+        self.dock = Some(dock);
+    }
+
+    pub fn set_model(&mut self, language_model: LanguageModelSelection) {
+        // let model = language_model.id().0.to_string();
+        // let provider = language_model.provider_id().0.to_string();
+        // self.default_model = Some(LanguageModelSelection {
+        //     provider: provider.into(),
+        //     model,
+        // });
+        self.default_model = Some(language_model)
+    }
+
+    pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
+        self.inline_assistant_model = Some(LanguageModelSelection {
+            provider: provider.into(),
+            model,
+        });
+    }
+
+    pub fn set_commit_message_model(&mut self, provider: String, model: String) {
+        self.commit_message_model = Some(LanguageModelSelection {
+            provider: provider.into(),
+            model,
+        });
+    }
+
+    pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
+        self.thread_summary_model = Some(LanguageModelSelection {
+            provider: provider.into(),
+            model,
+        });
+    }
+
+    pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
+        self.always_allow_tool_actions = Some(allow);
+    }
+
+    pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
+        self.play_sound_when_agent_done = Some(allow);
+    }
+
+    pub fn set_single_file_review(&mut self, allow: bool) {
+        self.single_file_review = Some(allow);
+    }
+
+    pub fn set_use_modifier_to_send(&mut self, always_use: bool) {
+        self.use_modifier_to_send = Some(always_use);
+    }
+
+    pub fn set_profile(&mut self, profile_id: Arc<str>) {
+        self.default_profile = Some(profile_id);
+    }
+}
+
+#[skip_serializing_none]
+#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct AgentProfileContent {
+    pub name: Arc<str>,
+    #[serde(default)]
+    pub tools: IndexMap<Arc<str>, bool>,
+    /// Whether all context servers are enabled by default.
+    pub enable_all_context_servers: Option<bool>,
+    #[serde(default)]
+    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
+}
+
+#[skip_serializing_none]
+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ContextServerPresetContent {
+    pub tools: IndexMap<Arc<str>, bool>,
+}
+
+#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DefaultAgentView {
+    #[default]
+    Thread,
+    TextThread,
+}
+
+#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum NotifyWhenAgentWaiting {
+    #[default]
+    PrimaryScreen,
+    AllScreens,
+    Never,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct LanguageModelSelection {
+    pub provider: LanguageModelProviderSetting,
+    pub model: String,
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum CompletionMode {
+    #[default]
+    Normal,
+    #[serde(alias = "max")]
+    Burn,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct LanguageModelParameters {
+    pub provider: Option<LanguageModelProviderSetting>,
+    pub model: Option<SharedString>,
+    pub temperature: Option<f32>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
+pub struct LanguageModelProviderSetting(pub String);
+
+impl JsonSchema for LanguageModelProviderSetting {
+    fn schema_name() -> Cow<'static, str> {
+        "LanguageModelProviderSetting".into()
+    }
+
+    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        // list the builtin providers as a subset so that we still auto complete them in the settings
+        json_schema!({
+            "anyOf": [
+                {
+                    "type": "string",
+                    "enum": [
+                        "amazon-bedrock",
+                        "anthropic",
+                        "copilot_chat",
+                        "deepseek",
+                        "google",
+                        "lmstudio",
+                        "mistral",
+                        "ollama",
+                        "openai",
+                        "openrouter",
+                        "vercel",
+                        "x_ai",
+                        "zed.dev"
+                    ]
+                },
+                {
+                    "type": "string",
+                }
+            ]
+        })
+    }
+}
+
+impl From<String> for LanguageModelProviderSetting {
+    fn from(provider: String) -> Self {
+        Self(provider)
+    }
+}
+
+impl From<&str> for LanguageModelProviderSetting {
+    fn from(provider: &str) -> Self {
+        Self(provider.to_string())
+    }
+}
+
+#[skip_serializing_none]
+#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, Debug)]
+pub struct AllAgentServersSettings {
+    pub gemini: Option<BuiltinAgentServerSettings>,
+    pub claude: Option<BuiltinAgentServerSettings>,
+
+    /// Custom agent servers configured by the user
+    #[serde(flatten)]
+    pub custom: HashMap<SharedString, CustomAgentServerSettings>,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+pub struct BuiltinAgentServerSettings {
+    /// Absolute path to a binary to be used when launching this agent.
+    ///
+    /// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
+    #[serde(rename = "command")]
+    pub path: Option<PathBuf>,
+    /// If a binary is specified in `command`, it will be passed these arguments.
+    pub args: Option<Vec<String>>,
+    /// If a binary is specified in `command`, it will be passed these environment variables.
+    pub env: Option<HashMap<String, String>>,
+    /// Whether to skip searching `$PATH` for an agent server binary when
+    /// launching this agent.
+    ///
+    /// This has no effect if a `command` is specified. Otherwise, when this is
+    /// `false`, Zed will search `$PATH` for an agent server binary and, if one
+    /// is found, use it for threads with this agent. If no agent binary is
+    /// found on `$PATH`, Zed will automatically install and use its own binary.
+    /// When this is `true`, Zed will not search `$PATH`, and will always use
+    /// its own binary.
+    ///
+    /// Default: true
+    pub ignore_system_version: Option<bool>,
+    /// The default mode to use for this agent.
+    ///
+    /// Note: Not only all agents support modes.
+    ///
+    /// Default: None
+    pub default_mode: Option<String>,
+}
+
+#[skip_serializing_none]
+#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+pub struct CustomAgentServerSettings {
+    #[serde(rename = "command")]
+    pub path: PathBuf,
+    #[serde(default)]
+    pub args: Vec<String>,
+    pub env: Option<HashMap<String, String>>,
+    /// The default mode to use for this agent.
+    ///
+    /// Note: Not only all agents support modes.
+    ///
+    /// Default: None
+    pub default_mode: Option<String>,
+}

crates/settings/src/settings_content/editor.rs πŸ”—

@@ -0,0 +1,603 @@
+use std::num;
+
+use collections::HashMap;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+
+use crate::{DiagnosticSeverityContent, ShowScrollbar};
+
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct EditorSettingsContent {
+    /// Whether the cursor blinks in the editor.
+    ///
+    /// Default: true
+    pub cursor_blink: Option<bool>,
+    /// Cursor shape for the default editor.
+    /// Can be "bar", "block", "underline", or "hollow".
+    ///
+    /// Default: bar
+    pub cursor_shape: Option<CursorShape>,
+    /// Determines when the mouse cursor should be hidden in an editor or input box.
+    ///
+    /// Default: on_typing_and_movement
+    pub hide_mouse: Option<HideMouseMode>,
+    /// Determines how snippets are sorted relative to other completion items.
+    ///
+    /// Default: inline
+    pub snippet_sort_order: Option<SnippetSortOrder>,
+    /// How to highlight the current line in the editor.
+    ///
+    /// Default: all
+    pub current_line_highlight: Option<CurrentLineHighlight>,
+    /// Whether to highlight all occurrences of the selected text in an editor.
+    ///
+    /// Default: true
+    pub selection_highlight: Option<bool>,
+    /// Whether the text selection should have rounded corners.
+    ///
+    /// Default: true
+    pub rounded_selection: Option<bool>,
+    /// The debounce delay before querying highlights from the language
+    /// server based on the current cursor location.
+    ///
+    /// Default: 75
+    pub lsp_highlight_debounce: Option<u64>,
+    /// Whether to show the informational hover box when moving the mouse
+    /// over symbols in the editor.
+    ///
+    /// Default: true
+    pub hover_popover_enabled: Option<bool>,
+    /// Time to wait in milliseconds before showing the informational hover box.
+    ///
+    /// Default: 300
+    pub hover_popover_delay: Option<u64>,
+    /// Status bar related settings
+    pub status_bar: Option<StatusBarContent>,
+    /// Toolbar related settings
+    pub toolbar: Option<ToolbarContent>,
+    /// Scrollbar related settings
+    pub scrollbar: Option<ScrollbarContent>,
+    /// Minimap related settings
+    pub minimap: Option<MinimapContent>,
+    /// Gutter related settings
+    pub gutter: Option<GutterContent>,
+    /// Whether the editor will scroll beyond the last line.
+    ///
+    /// Default: one_page
+    pub scroll_beyond_last_line: Option<ScrollBeyondLastLine>,
+    /// The number of lines to keep above/below the cursor when auto-scrolling.
+    ///
+    /// Default: 3.
+    pub vertical_scroll_margin: Option<f32>,
+    /// Whether to scroll when clicking near the edge of the visible text area.
+    ///
+    /// Default: false
+    pub autoscroll_on_clicks: Option<bool>,
+    /// The number of characters to keep on either side when scrolling with the mouse.
+    ///
+    /// Default: 5.
+    pub horizontal_scroll_margin: Option<f32>,
+    /// Scroll sensitivity multiplier. This multiplier is applied
+    /// to both the horizontal and vertical delta values while scrolling.
+    ///
+    /// Default: 1.0
+    pub scroll_sensitivity: Option<f32>,
+    /// Scroll sensitivity multiplier for fast scrolling. This multiplier is applied
+    /// to both the horizontal and vertical delta values while scrolling. Fast scrolling
+    /// happens when a user holds the alt or option key while scrolling.
+    ///
+    /// Default: 4.0
+    pub fast_scroll_sensitivity: Option<f32>,
+    /// Whether the line numbers on editors gutter are relative or not.
+    ///
+    /// Default: false
+    pub relative_line_numbers: Option<bool>,
+    /// When to populate a new search's query based on the text under the cursor.
+    ///
+    /// Default: always
+    pub seed_search_query_from_cursor: Option<SeedQuerySetting>,
+    pub use_smartcase_search: Option<bool>,
+    /// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier.
+    ///
+    /// Default: alt
+    pub multi_cursor_modifier: Option<MultiCursorModifier>,
+    /// Hide the values of variables in `private` files, as defined by the
+    /// private_files setting. This only changes the visual representation,
+    /// the values are still present in the file and can be selected / copied / pasted
+    ///
+    /// Default: false
+    pub redact_private_values: Option<bool>,
+
+    /// How many lines to expand the multibuffer excerpts by default
+    ///
+    /// Default: 3
+    pub expand_excerpt_lines: Option<u32>,
+
+    /// How many lines of context to provide in multibuffer excerpts by default
+    ///
+    /// Default: 2
+    pub excerpt_context_lines: Option<u32>,
+
+    /// Whether to enable middle-click paste on Linux
+    ///
+    /// Default: true
+    pub middle_click_paste: Option<bool>,
+
+    /// What to do when multibuffer is double clicked in some of its excerpts
+    /// (parts of singleton buffers).
+    ///
+    /// Default: select
+    pub double_click_in_multibuffer: Option<DoubleClickInMultibuffer>,
+    /// Whether the editor search results will loop
+    ///
+    /// Default: true
+    pub search_wrap: Option<bool>,
+
+    /// Defaults to use when opening a new buffer and project search items.
+    ///
+    /// Default: nothing is enabled
+    pub search: Option<SearchSettingsContent>,
+
+    /// Whether to automatically show a signature help pop-up or not.
+    ///
+    /// Default: false
+    pub auto_signature_help: Option<bool>,
+
+    /// Whether to show the signature help pop-up after completions or bracket pairs inserted.
+    ///
+    /// Default: false
+    pub show_signature_help_after_edits: Option<bool>,
+    /// The minimum APCA perceptual contrast to maintain when
+    /// rendering text over highlight backgrounds in the editor.
+    ///
+    /// Values range from 0 to 106. Set to 0 to disable adjustments.
+    /// Default: 45
+    pub minimum_contrast_for_highlights: Option<f32>,
+
+    /// Whether to follow-up empty go to definition responses from the language server or not.
+    /// `FindAllReferences` allows to look up references of the same symbol instead.
+    /// `None` disables the fallback.
+    ///
+    /// Default: FindAllReferences
+    pub go_to_definition_fallback: Option<GoToDefinitionFallback>,
+
+    /// Jupyter REPL settings.
+    pub jupyter: Option<JupyterContent>,
+
+    /// Which level to use to filter out diagnostics displayed in the editor.
+    ///
+    /// Affects the editor rendering only, and does not interrupt
+    /// the functionality of diagnostics fetching and project diagnostics editor.
+    /// Which files containing diagnostic errors/warnings to mark in the tabs.
+    /// Diagnostics are only shown when file icons are also active.
+    ///
+    /// Shows all diagnostics if not specified.
+    ///
+    /// Default: warning
+    pub diagnostics_max_severity: Option<DiagnosticSeverityContent>,
+
+    /// Whether to show code action button at start of buffer line.
+    ///
+    /// Default: true
+    pub inline_code_actions: Option<bool>,
+
+    /// Drag and drop related settings
+    pub drag_and_drop_selection: Option<DragAndDropSelectionContent>,
+
+    /// How to render LSP `textDocument/documentColor` colors in the editor.
+    ///
+    /// Default: [`DocumentColorsRenderMode::Inlay`]
+    pub lsp_document_colors: Option<DocumentColorsRenderMode>,
+}
+
+// Status bar related settings
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct StatusBarContent {
+    /// Whether to display the active language button in the status bar.
+    ///
+    /// Default: true
+    pub active_language_button: Option<bool>,
+    /// Whether to show the cursor position button in the status bar.
+    ///
+    /// Default: true
+    pub cursor_position_button: Option<bool>,
+}
+
+// Toolbar related settings
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ToolbarContent {
+    /// Whether to display breadcrumbs in the editor toolbar.
+    ///
+    /// Default: true
+    pub breadcrumbs: Option<bool>,
+    /// Whether to display quick action buttons in the editor toolbar.
+    ///
+    /// Default: true
+    pub quick_actions: Option<bool>,
+    /// Whether to show the selections menu in the editor toolbar.
+    ///
+    /// Default: true
+    pub selections_menu: Option<bool>,
+    /// Whether to display Agent review buttons in the editor toolbar.
+    /// Only applicable while reviewing a file edited by the Agent.
+    ///
+    /// Default: true
+    pub agent_review: Option<bool>,
+    /// Whether to display code action buttons in the editor toolbar.
+    ///
+    /// Default: false
+    pub code_actions: Option<bool>,
+}
+
+/// Scrollbar related settings
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
+pub struct ScrollbarContent {
+    /// When to show the scrollbar in the editor.
+    ///
+    /// Default: auto
+    pub show: Option<ShowScrollbar>,
+    /// Whether to show git diff indicators in the scrollbar.
+    ///
+    /// Default: true
+    pub git_diff: Option<bool>,
+    /// Whether to show buffer search result indicators in the scrollbar.
+    ///
+    /// Default: true
+    pub search_results: Option<bool>,
+    /// Whether to show selected text occurrences in the scrollbar.
+    ///
+    /// Default: true
+    pub selected_text: Option<bool>,
+    /// Whether to show selected symbol occurrences in the scrollbar.
+    ///
+    /// Default: true
+    pub selected_symbol: Option<bool>,
+    /// Which diagnostic indicators to show in the scrollbar:
+    ///
+    /// Default: all
+    pub diagnostics: Option<ScrollbarDiagnostics>,
+    /// Whether to show cursor positions in the scrollbar.
+    ///
+    /// Default: true
+    pub cursors: Option<bool>,
+    /// Forcefully enable or disable the scrollbar for each axis
+    pub axes: Option<ScrollbarAxesContent>,
+}
+
+/// Minimap related settings
+#[skip_serializing_none]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct MinimapContent {
+    /// When to show the minimap in the editor.
+    ///
+    /// Default: never
+    pub show: Option<ShowMinimap>,
+
+    /// Where to show the minimap in the editor.
+    ///
+    /// Default: [`DisplayIn::ActiveEditor`]
+    pub display_in: Option<DisplayIn>,
+
+    /// When to show the minimap thumb.
+    ///
+    /// Default: always
+    pub thumb: Option<MinimapThumb>,
+
+    /// Defines the border style for the minimap's scrollbar thumb.
+    ///
+    /// Default: left_open
+    pub thumb_border: Option<MinimapThumbBorder>,
+
+    /// How to highlight the current line in the minimap.
+    ///
+    /// Default: inherits editor line highlights setting
+    pub current_line_highlight: Option<Option<CurrentLineHighlight>>,
+
+    /// Maximum number of columns to display in the minimap.
+    ///
+    /// Default: 80
+    pub max_width_columns: Option<num::NonZeroU32>,
+}
+
+/// Forcefully enable or disable the scrollbar for each axis
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
+pub struct ScrollbarAxesContent {
+    /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
+    ///
+    /// Default: true
+    pub horizontal: Option<bool>,
+
+    /// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings.
+    ///
+    /// Default: true
+    pub vertical: Option<bool>,
+}
+
+/// Gutter related settings
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct GutterContent {
+    /// Whether to show line numbers in the gutter.
+    ///
+    /// Default: true
+    pub line_numbers: Option<bool>,
+    /// Minimum number of characters to reserve space for in the gutter.
+    ///
+    /// Default: 4
+    pub min_line_number_digits: Option<usize>,
+    /// Whether to show runnable buttons in the gutter.
+    ///
+    /// Default: true
+    pub runnables: Option<bool>,
+    /// Whether to show breakpoints in the gutter.
+    ///
+    /// Default: true
+    pub breakpoints: Option<bool>,
+    /// Whether to show fold buttons in the gutter.
+    ///
+    /// Default: true
+    pub folds: Option<bool>,
+}
+
+/// How to render LSP `textDocument/documentColor` colors in the editor.
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DocumentColorsRenderMode {
+    /// Do not query and render document colors.
+    None,
+    /// Render document colors as inlay hints near the color text.
+    #[default]
+    Inlay,
+    /// Draw a border around the color text.
+    Border,
+    /// Draw a background behind the color text.
+    Background,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum CurrentLineHighlight {
+    // Don't highlight the current line.
+    None,
+    // Highlight the gutter area.
+    Gutter,
+    // Highlight the editor area.
+    Line,
+    // Highlight the full line.
+    All,
+}
+
+/// When to populate a new search's query based on the text under the cursor.
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SeedQuerySetting {
+    /// Always populate the search query with the word under the cursor.
+    Always,
+    /// Only populate the search query when there is text selected.
+    Selection,
+    /// Never populate the search query
+    Never,
+}
+
+/// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers).
+#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DoubleClickInMultibuffer {
+    /// Behave as a regular buffer and select the whole word.
+    #[default]
+    Select,
+    /// Open the excerpt clicked as a new buffer in the new tab, if no `alt` modifier was pressed during double click.
+    /// Otherwise, behave as a regular buffer and select the whole word.
+    Open,
+}
+
+/// When to show the minimap thumb.
+///
+/// Default: always
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum MinimapThumb {
+    /// Show the minimap thumb only when the mouse is hovering over the minimap.
+    Hover,
+    /// Always show the minimap thumb.
+    #[default]
+    Always,
+}
+
+/// Defines the border style for the minimap's scrollbar thumb.
+///
+/// Default: left_open
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum MinimapThumbBorder {
+    /// Displays a border on all sides of the thumb.
+    Full,
+    /// Displays a border on all sides except the left side of the thumb.
+    #[default]
+    LeftOpen,
+    /// Displays a border on all sides except the right side of the thumb.
+    RightOpen,
+    /// Displays a border only on the left side of the thumb.
+    LeftOnly,
+    /// Displays the thumb without any border.
+    None,
+}
+
+/// Which diagnostic indicators to show in the scrollbar.
+///
+/// Default: all
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "lowercase")]
+pub enum ScrollbarDiagnostics {
+    /// Show all diagnostic levels: hint, information, warnings, error.
+    All,
+    /// Show only the following diagnostic levels: information, warning, error.
+    Information,
+    /// Show only the following diagnostic levels: warning, error.
+    Warning,
+    /// Show only the following diagnostic level: error.
+    Error,
+    /// Do not show diagnostics.
+    None,
+}
+
+/// The key to use for adding multiple cursors
+///
+/// Default: alt
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum MultiCursorModifier {
+    Alt,
+    #[serde(alias = "cmd", alias = "ctrl")]
+    CmdOrCtrl,
+}
+
+/// Whether the editor will scroll beyond the last line.
+///
+/// Default: one_page
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ScrollBeyondLastLine {
+    /// The editor will not scroll beyond the last line.
+    Off,
+
+    /// The editor will scroll beyond the last line by one page.
+    OnePage,
+
+    /// The editor will scroll beyond the last line by the same number of lines as vertical_scroll_margin.
+    VerticalScrollMargin,
+}
+
+/// The shape of a selection cursor.
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum CursorShape {
+    /// A vertical bar
+    #[default]
+    Bar,
+    /// A block that surrounds the following character
+    Block,
+    /// An underline that runs along the following character
+    Underline,
+    /// A box drawn around the following character
+    Hollow,
+}
+
+/// What to do when go to definition yields no results.
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GoToDefinitionFallback {
+    /// Disables the fallback.
+    None,
+    /// Looks up references of the same symbol instead.
+    #[default]
+    FindAllReferences,
+}
+
+/// Determines when the mouse cursor should be hidden in an editor or input box.
+///
+/// Default: on_typing_and_movement
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum HideMouseMode {
+    /// Never hide the mouse cursor
+    Never,
+    /// Hide only when typing
+    OnTyping,
+    /// Hide on both typing and cursor movement
+    #[default]
+    OnTypingAndMovement,
+}
+
+/// Determines how snippets are sorted relative to other completion items.
+///
+/// Default: inline
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SnippetSortOrder {
+    /// Place snippets at the top of the completion list
+    Top,
+    /// Sort snippets normally using the default comparison logic
+    #[default]
+    Inline,
+    /// Place snippets at the bottom of the completion list
+    Bottom,
+    /// Do not show snippets in the completion list
+    None,
+}
+
+/// Default options for buffer and project search items.
+#[skip_serializing_none]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct SearchSettingsContent {
+    /// Whether to show the project search button in the status bar.
+    pub button: Option<bool>,
+    pub whole_word: Option<bool>,
+    pub case_sensitive: Option<bool>,
+    pub include_ignored: Option<bool>,
+    pub regex: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct JupyterContent {
+    /// Whether the Jupyter feature is enabled.
+    ///
+    /// Default: true
+    pub enabled: Option<bool>,
+
+    /// Default kernels to select for each language.
+    ///
+    /// Default: `{}`
+    pub kernel_selections: Option<HashMap<String, String>>,
+}
+
+/// Whether to allow drag and drop text selection in buffer.
+#[skip_serializing_none]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct DragAndDropSelectionContent {
+    /// When true, enables drag and drop text selection in buffer.
+    ///
+    /// Default: true
+    pub enabled: Option<bool>,
+
+    /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created.
+    ///
+    /// Default: 300
+    pub delay: Option<u64>,
+}
+
+/// When to show the minimap in the editor.
+///
+/// Default: never
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowMinimap {
+    /// Follow the visibility of the scrollbar.
+    Auto,
+    /// Always show the minimap.
+    Always,
+    /// Never show the minimap.
+    #[default]
+    Never,
+}
+
+/// Where to show the minimap in the editor.
+///
+/// Default: all_editors
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum DisplayIn {
+    /// Show on all open editors.
+    AllEditors,
+    /// Show the minimap on the active editor only.
+    #[default]
+    ActiveEditor,
+}

crates/settings/src/settings_content/language.rs πŸ”—

@@ -0,0 +1,894 @@
+use std::{borrow::Cow, num::NonZeroU32};
+
+use collections::{HashMap, HashSet};
+use gpui::{Modifiers, SharedString};
+use schemars::{JsonSchema, json_schema};
+use serde::{
+    Deserialize, Deserializer, Serialize,
+    de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor},
+};
+use serde_with::skip_serializing_none;
+use std::sync::Arc;
+use util::schemars::replace_subschema;
+
+use crate::ParameterizedJsonSchema;
+
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct AllLanguageSettingsContent {
+    /// The settings for enabling/disabling features.
+    #[serde(default)]
+    pub features: Option<FeaturesContent>,
+    /// The edit prediction settings.
+    #[serde(default)]
+    pub edit_predictions: Option<EditPredictionSettingsContent>,
+    /// The default language settings.
+    #[serde(flatten)]
+    pub defaults: LanguageSettingsContent,
+    /// The settings for individual languages.
+    #[serde(default)]
+    pub languages: LanguageToSettingsMap,
+    /// Settings for associating file extensions and filenames
+    /// with languages.
+    #[serde(default)]
+    pub file_types: HashMap<Arc<str>, Vec<String>>,
+}
+
+/// The settings for enabling/disabling features.
+#[skip_serializing_none]
+#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct FeaturesContent {
+    /// Determines which edit prediction provider to use.
+    pub edit_prediction_provider: Option<EditPredictionProvider>,
+}
+
+/// The provider that supplies edit predictions.
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum EditPredictionProvider {
+    None,
+    #[default]
+    Copilot,
+    Supermaven,
+    Zed,
+}
+
+/// The contents of the edit prediction settings.
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct EditPredictionSettingsContent {
+    /// A list of globs representing files that edit predictions should be disabled for.
+    /// This list adds to a pre-existing, sensible default set of globs.
+    /// Any additional ones you add are combined with them.
+    pub disabled_globs: Option<Vec<String>>,
+    /// The mode used to display edit predictions in the buffer.
+    /// Provider support required.
+    pub mode: Option<EditPredictionsMode>,
+    /// Settings specific to GitHub Copilot.
+    pub copilot: Option<CopilotSettingsContent>,
+    /// Whether edit predictions are enabled in the assistant prompt editor.
+    /// This has no effect if globally disabled.
+    pub enabled_in_text_threads: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct CopilotSettingsContent {
+    /// HTTP/HTTPS proxy to use for Copilot.
+    ///
+    /// Default: none
+    pub proxy: Option<String>,
+    /// Disable certificate verification for the proxy (not recommended).
+    ///
+    /// Default: false
+    pub proxy_no_verify: Option<bool>,
+    /// Enterprise URI for Copilot.
+    ///
+    /// Default: none
+    pub enterprise_uri: Option<String>,
+}
+
+/// The mode in which edit predictions should be displayed.
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum EditPredictionsMode {
+    /// If provider supports it, display inline when holding modifier key (e.g., alt).
+    /// Otherwise, eager preview is used.
+    #[serde(alias = "auto")]
+    Subtle,
+    /// Display inline when there are no language server completions available.
+    #[default]
+    #[serde(alias = "eager_preview")]
+    Eager,
+}
+
+/// Controls the soft-wrapping behavior in the editor.
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SoftWrap {
+    /// Prefer a single line generally, unless an overly long line is encountered.
+    None,
+    /// Deprecated: use None instead. Left to avoid breaking existing users' configs.
+    /// Prefer a single line generally, unless an overly long line is encountered.
+    PreferLine,
+    /// Soft wrap lines that exceed the editor width.
+    EditorWidth,
+    /// Soft wrap lines at the preferred line length.
+    PreferredLineLength,
+    /// Soft wrap line at the preferred line length or the editor width (whichever is smaller).
+    Bounded,
+}
+
+/// The settings for a particular language.
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct LanguageSettingsContent {
+    /// How many columns a tab should occupy.
+    ///
+    /// Default: 4
+    pub tab_size: Option<NonZeroU32>,
+    /// Whether to indent lines using tab characters, as opposed to multiple
+    /// spaces.
+    ///
+    /// Default: false
+    pub hard_tabs: Option<bool>,
+    /// How to soft-wrap long lines of text.
+    ///
+    /// Default: none
+    pub soft_wrap: Option<SoftWrap>,
+    /// The column at which to soft-wrap lines, for buffers where soft-wrap
+    /// is enabled.
+    ///
+    /// Default: 80
+    pub preferred_line_length: Option<u32>,
+    /// Whether to show wrap guides in the editor. Setting this to true will
+    /// show a guide at the 'preferred_line_length' value if softwrap is set to
+    /// 'preferred_line_length', and will show any additional guides as specified
+    /// by the 'wrap_guides' setting.
+    ///
+    /// Default: true
+    pub show_wrap_guides: Option<bool>,
+    /// Character counts at which to show wrap guides in the editor.
+    ///
+    /// Default: []
+    pub wrap_guides: Option<Vec<usize>>,
+    /// Indent guide related settings.
+    pub indent_guides: Option<IndentGuideSettingsContent>,
+    /// Whether or not to perform a buffer format before saving.
+    ///
+    /// Default: on
+    pub format_on_save: Option<FormatOnSave>,
+    /// Whether or not to remove any trailing whitespace from lines of a buffer
+    /// before saving it.
+    ///
+    /// Default: true
+    pub remove_trailing_whitespace_on_save: Option<bool>,
+    /// Whether or not to ensure there's a single newline at the end of a buffer
+    /// when saving it.
+    ///
+    /// Default: true
+    pub ensure_final_newline_on_save: Option<bool>,
+    /// How to perform a buffer format.
+    ///
+    /// Default: auto
+    pub formatter: Option<SelectedFormatter>,
+    /// Zed's Prettier integration settings.
+    /// Allows to enable/disable formatting with Prettier
+    /// and configure default Prettier, used when no project-level Prettier installation is found.
+    ///
+    /// Default: off
+    pub prettier: Option<PrettierSettingsContent>,
+    /// Whether to automatically close JSX tags.
+    pub jsx_tag_auto_close: Option<JsxTagAutoCloseSettingsContent>,
+    /// Whether to use language servers to provide code intelligence.
+    ///
+    /// Default: true
+    pub enable_language_server: Option<bool>,
+    /// The list of language servers to use (or disable) for this language.
+    ///
+    /// This array should consist of language server IDs, as well as the following
+    /// special tokens:
+    /// - `"!<language_server_id>"` - A language server ID prefixed with a `!` will be disabled.
+    /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language.
+    ///
+    /// Default: ["..."]
+    pub language_servers: Option<Vec<String>>,
+    /// Controls where the `editor::Rewrap` action is allowed for this language.
+    ///
+    /// Note: This setting has no effect in Vim mode, as rewrap is already
+    /// allowed everywhere.
+    ///
+    /// Default: "in_comments"
+    pub allow_rewrap: Option<RewrapBehavior>,
+    /// Controls whether edit predictions are shown immediately (true)
+    /// or manually by triggering `editor::ShowEditPrediction` (false).
+    ///
+    /// Default: true
+    pub show_edit_predictions: Option<bool>,
+    /// Controls whether edit predictions are shown in the given language
+    /// scopes.
+    ///
+    /// Example: ["string", "comment"]
+    ///
+    /// Default: []
+    pub edit_predictions_disabled_in: Option<Vec<String>>,
+    /// Whether to show tabs and spaces in the editor.
+    pub show_whitespaces: Option<ShowWhitespaceSetting>,
+    /// Visible characters used to render whitespace when show_whitespaces is enabled.
+    ///
+    /// Default: "β€’" for spaces, "β†’" for tabs.
+    pub whitespace_map: Option<WhitespaceMap>,
+    /// Whether to start a new line with a comment when a previous line is a comment as well.
+    ///
+    /// Default: true
+    pub extend_comment_on_newline: Option<bool>,
+    /// Inlay hint related settings.
+    pub inlay_hints: Option<InlayHintSettingsContent>,
+    /// Whether to automatically type closing characters for you. For example,
+    /// when you type (, Zed will automatically add a closing ) at the correct position.
+    ///
+    /// Default: true
+    pub use_autoclose: Option<bool>,
+    /// Whether to automatically surround text with characters for you. For example,
+    /// when you select text and type (, Zed will automatically surround text with ().
+    ///
+    /// Default: true
+    pub use_auto_surround: Option<bool>,
+    /// Controls how the editor handles the autoclosed characters.
+    /// When set to `false`(default), skipping over and auto-removing of the closing characters
+    /// happen only for auto-inserted characters.
+    /// Otherwise(when `true`), the closing characters are always skipped over and auto-removed
+    /// no matter how they were inserted.
+    ///
+    /// Default: false
+    pub always_treat_brackets_as_autoclosed: Option<bool>,
+    /// Whether to use additional LSP queries to format (and amend) the code after
+    /// every "trigger" symbol input, defined by LSP server capabilities.
+    ///
+    /// Default: true
+    pub use_on_type_format: Option<bool>,
+    /// Which code actions to run on save after the formatter.
+    /// These are not run if formatting is off.
+    ///
+    /// Default: {} (or {"source.organizeImports": true} for Go).
+    pub code_actions_on_format: Option<HashMap<String, bool>>,
+    /// Whether to perform linked edits of associated ranges, if the language server supports it.
+    /// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
+    ///
+    /// Default: true
+    pub linked_edits: Option<bool>,
+    /// Whether indentation should be adjusted based on the context whilst typing.
+    ///
+    /// Default: true
+    pub auto_indent: Option<bool>,
+    /// Whether indentation of pasted content should be adjusted based on the context.
+    ///
+    /// Default: true
+    pub auto_indent_on_paste: Option<bool>,
+    /// Task configuration for this language.
+    ///
+    /// Default: {}
+    pub tasks: Option<LanguageTaskSettingsContent>,
+    /// Whether to pop the completions menu while typing in an editor without
+    /// explicitly requesting it.
+    ///
+    /// Default: true
+    pub show_completions_on_input: Option<bool>,
+    /// Whether to display inline and alongside documentation for items in the
+    /// completions menu.
+    ///
+    /// Default: true
+    pub show_completion_documentation: Option<bool>,
+    /// Controls how completions are processed for this language.
+    pub completions: Option<CompletionSettingsContent>,
+    /// Preferred debuggers for this language.
+    ///
+    /// Default: []
+    pub debuggers: Option<Vec<String>>,
+}
+
+/// Controls how whitespace should be displayedin the editor.
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowWhitespaceSetting {
+    /// Draw whitespace only for the selected text.
+    Selection,
+    /// Do not draw any tabs or spaces.
+    None,
+    /// Draw all invisible symbols.
+    All,
+    /// Draw whitespaces at boundaries only.
+    ///
+    /// For a whitespace to be on a boundary, any of the following conditions need to be met:
+    /// - It is a tab
+    /// - It is adjacent to an edge (start or end)
+    /// - It is adjacent to a whitespace (left or right)
+    Boundary,
+    /// Draw whitespaces only after non-whitespace characters.
+    Trailing,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct WhitespaceMap {
+    pub space: Option<String>,
+    pub tab: Option<String>,
+}
+
+impl WhitespaceMap {
+    pub fn space(&self) -> SharedString {
+        self.space
+            .as_ref()
+            .map_or_else(|| SharedString::from("β€’"), |s| SharedString::from(s))
+    }
+
+    pub fn tab(&self) -> SharedString {
+        self.tab
+            .as_ref()
+            .map_or_else(|| SharedString::from("β†’"), |s| SharedString::from(s))
+    }
+}
+
+/// The behavior of `editor::Rewrap`.
+#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum RewrapBehavior {
+    /// Only rewrap within comments.
+    #[default]
+    InComments,
+    /// Only rewrap within the current selection(s).
+    InSelections,
+    /// Allow rewrapping anywhere.
+    Anywhere,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct JsxTagAutoCloseSettingsContent {
+    /// Enables or disables auto-closing of JSX tags.
+    pub enabled: Option<bool>,
+}
+
+/// The settings for inlay hints.
+#[skip_serializing_none]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct InlayHintSettingsContent {
+    /// Global switch to toggle hints on and off.
+    ///
+    /// Default: false
+    pub enabled: Option<bool>,
+    /// Global switch to toggle inline values on and off when debugging.
+    ///
+    /// Default: true
+    pub show_value_hints: Option<bool>,
+    /// Whether type hints should be shown.
+    ///
+    /// Default: true
+    pub show_type_hints: Option<bool>,
+    /// Whether parameter hints should be shown.
+    ///
+    /// Default: true
+    pub show_parameter_hints: Option<bool>,
+    /// Whether other hints should be shown.
+    ///
+    /// Default: true
+    pub show_other_hints: Option<bool>,
+    /// Whether to show a background for inlay hints.
+    ///
+    /// If set to `true`, the background will use the `hint.background` color
+    /// from the current theme.
+    ///
+    /// Default: false
+    pub show_background: Option<bool>,
+    /// Whether or not to debounce inlay hints updates after buffer edits.
+    ///
+    /// Set to 0 to disable debouncing.
+    ///
+    /// Default: 700
+    pub edit_debounce_ms: Option<u64>,
+    /// Whether or not to debounce inlay hints updates after buffer scrolls.
+    ///
+    /// Set to 0 to disable debouncing.
+    ///
+    /// Default: 50
+    pub scroll_debounce_ms: Option<u64>,
+    /// Toggles inlay hints (hides or shows) when the user presses the modifiers specified.
+    /// If only a subset of the modifiers specified is pressed, hints are not toggled.
+    /// If no modifiers are specified, this is equivalent to `null`.
+    ///
+    /// Default: null
+    pub toggle_on_modifiers_press: Option<Modifiers>,
+}
+
+/// The kind of an inlay hint.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum InlayHintKind {
+    /// An inlay hint for a type.
+    Type,
+    /// An inlay hint for a parameter.
+    Parameter,
+}
+
+impl InlayHintKind {
+    /// Returns the [`InlayHintKind`]fromthe given name.
+    ///
+    /// Returns `None` if `name` does not match any of the expected
+    /// string representations.
+    pub fn from_name(name: &str) -> Option<Self> {
+        match name {
+            "type" => Some(InlayHintKind::Type),
+            "parameter" => Some(InlayHintKind::Parameter),
+            _ => None,
+        }
+    }
+
+    /// Returns the name of this [`InlayHintKind`].
+    pub fn name(&self) -> &'static str {
+        match self {
+            InlayHintKind::Type => "type",
+            InlayHintKind::Parameter => "parameter",
+        }
+    }
+}
+
+/// Controls how completions are processed for this language.
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[serde(rename_all = "snake_case")]
+pub struct CompletionSettingsContent {
+    /// Controls how words are completed.
+    /// For large documents, not all words may be fetched for completion.
+    ///
+    /// Default: `fallback`
+    pub words: Option<WordsCompletionMode>,
+    /// How many characters has to be in the completions query to automatically show the words-based completions.
+    /// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
+    ///
+    /// Default: 3
+    pub words_min_length: Option<usize>,
+    /// Whether to fetch LSP completions or not.
+    ///
+    /// Default: true
+    pub lsp: Option<bool>,
+    /// When fetching LSP completions, determines how long to wait for a response of a particular server.
+    /// When set to 0, waits indefinitely.
+    ///
+    /// Default: 0
+    pub lsp_fetch_timeout_ms: Option<u64>,
+    /// Controls how LSP completions are inserted.
+    ///
+    /// Default: "replace_suffix"
+    pub lsp_insert_mode: Option<LspInsertMode>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum LspInsertMode {
+    /// Replaces text before the cursor, using the `insert` range described in the LSP specification.
+    Insert,
+    /// Replaces text before and after the cursor, using the `replace` range described in the LSP specification.
+    Replace,
+    /// Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text,
+    /// and like `"insert"` otherwise.
+    ReplaceSubsequence,
+    /// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like
+    /// `"insert"` otherwise.
+    ReplaceSuffix,
+}
+
+/// Controls how document's words are completed.
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum WordsCompletionMode {
+    /// Always fetch document's words for completions along with LSP completions.
+    Enabled,
+    /// Only if LSP response errors or times out,
+    /// use document's words to show completions.
+    Fallback,
+    /// Never fetch or complete document's words for completions.
+    /// (Word-based completions can still be queried via a separate action)
+    Disabled,
+}
+
+/// Allows to enable/disable formatting with Prettier
+/// and configure default Prettier, used when no project-level Prettier installation is found.
+/// Prettier formatting is disabled by default.
+#[skip_serializing_none]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct PrettierSettingsContent {
+    /// Enables or disables formatting with Prettier for a given language.
+    pub allowed: Option<bool>,
+
+    /// Forces Prettier integration to use a specific parser name when formatting files with the language.
+    pub parser: Option<String>,
+
+    /// Forces Prettier integration to use specific plugins when formatting files with the language.
+    /// The default Prettier will be installed with these plugins.
+    #[serde(default)]
+    pub plugins: HashSet<String>,
+
+    /// Default Prettier options, in the format as in package.json section for Prettier.
+    /// If project installs Prettier via its package.json, these options will be ignored.
+    #[serde(flatten)]
+    pub options: HashMap<String, serde_json::Value>,
+}
+
+/// Controls the behavior of formatting files when they are saved.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum FormatOnSave {
+    /// Files should be formatted on save.
+    On,
+    /// Files should not be formatted on save.
+    Off,
+    List(FormatterList),
+}
+
+impl JsonSchema for FormatOnSave {
+    fn schema_name() -> Cow<'static, str> {
+        "OnSaveFormatter".into()
+    }
+
+    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        let formatter_schema = Formatter::json_schema(generator);
+
+        json_schema!({
+            "oneOf": [
+                {
+                    "type": "array",
+                    "items": formatter_schema
+                },
+                {
+                    "type": "string",
+                    "enum": ["on", "off", "language_server"]
+                },
+                formatter_schema
+            ]
+        })
+    }
+}
+
+impl Serialize for FormatOnSave {
+    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        match self {
+            Self::On => serializer.serialize_str("on"),
+            Self::Off => serializer.serialize_str("off"),
+            Self::List(list) => list.serialize(serializer),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for FormatOnSave {
+    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct FormatDeserializer;
+
+        impl<'d> Visitor<'d> for FormatDeserializer {
+            type Value = FormatOnSave;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("a valid on-save formatter kind")
+            }
+            fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                if v == "on" {
+                    Ok(Self::Value::On)
+                } else if v == "off" {
+                    Ok(Self::Value::Off)
+                } else if v == "language_server" {
+                    Ok(Self::Value::List(FormatterList::Single(
+                        Formatter::LanguageServer { name: None },
+                    )))
+                } else {
+                    let ret: Result<FormatterList, _> =
+                        Deserialize::deserialize(v.into_deserializer());
+                    ret.map(Self::Value::List)
+                }
+            }
+            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
+            where
+                A: MapAccess<'d>,
+            {
+                let ret: Result<FormatterList, _> =
+                    Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
+                ret.map(Self::Value::List)
+            }
+            fn visit_seq<A>(self, map: A) -> Result<Self::Value, A::Error>
+            where
+                A: SeqAccess<'d>,
+            {
+                let ret: Result<FormatterList, _> =
+                    Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
+                ret.map(Self::Value::List)
+            }
+        }
+        deserializer.deserialize_any(FormatDeserializer)
+    }
+}
+
+/// Controls which formatter should be used when formatting code.
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub enum SelectedFormatter {
+    /// Format files using Zed's Prettier integration (if applicable),
+    /// or falling back to formatting via language server.
+    #[default]
+    Auto,
+    List(FormatterList),
+}
+
+impl JsonSchema for SelectedFormatter {
+    fn schema_name() -> Cow<'static, str> {
+        "Formatter".into()
+    }
+
+    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+        let formatter_schema = Formatter::json_schema(generator);
+
+        json_schema!({
+            "oneOf": [
+                {
+                    "type": "array",
+                    "items": formatter_schema
+                },
+                {
+                    "type": "string",
+                    "enum": ["auto", "language_server"]
+                },
+                formatter_schema
+            ]
+        })
+    }
+}
+
+impl Serialize for SelectedFormatter {
+    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        match self {
+            SelectedFormatter::Auto => serializer.serialize_str("auto"),
+            SelectedFormatter::List(list) => list.serialize(serializer),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for SelectedFormatter {
+    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct FormatDeserializer;
+
+        impl<'d> Visitor<'d> for FormatDeserializer {
+            type Value = SelectedFormatter;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("a valid formatter kind")
+            }
+            fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                if v == "auto" {
+                    Ok(Self::Value::Auto)
+                } else if v == "language_server" {
+                    Ok(Self::Value::List(FormatterList::Single(
+                        Formatter::LanguageServer { name: None },
+                    )))
+                } else {
+                    let ret: Result<FormatterList, _> =
+                        Deserialize::deserialize(v.into_deserializer());
+                    ret.map(SelectedFormatter::List)
+                }
+            }
+            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
+            where
+                A: MapAccess<'d>,
+            {
+                let ret: Result<FormatterList, _> =
+                    Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
+                ret.map(SelectedFormatter::List)
+            }
+            fn visit_seq<A>(self, map: A) -> Result<Self::Value, A::Error>
+            where
+                A: SeqAccess<'d>,
+            {
+                let ret: Result<FormatterList, _> =
+                    Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
+                ret.map(SelectedFormatter::List)
+            }
+        }
+        deserializer.deserialize_any(FormatDeserializer)
+    }
+}
+
+/// Controls which formatters should be used when formatting code.
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(untagged)]
+pub enum FormatterList {
+    Single(Formatter),
+    Vec(Vec<Formatter>),
+}
+
+impl Default for FormatterList {
+    fn default() -> Self {
+        Self::Single(Formatter::default())
+    }
+}
+
+impl AsRef<[Formatter]> for FormatterList {
+    fn as_ref(&self) -> &[Formatter] {
+        match &self {
+            Self::Single(single) => std::slice::from_ref(single),
+            Self::Vec(v) => v,
+        }
+    }
+}
+
+/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration.
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Formatter {
+    /// Format code using the current language server.
+    LanguageServer { name: Option<String> },
+    /// Format code using Zed's Prettier integration.
+    #[default]
+    Prettier,
+    /// Format code using an external command.
+    External {
+        /// The external program to run.
+        command: Arc<str>,
+        /// The arguments to pass to the program.
+        arguments: Option<Arc<[String]>>,
+    },
+    /// Files should be formatted using code actions executed by language servers.
+    CodeActions(HashMap<String, bool>),
+}
+
+/// The settings for indent guides.
+#[skip_serializing_none]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct IndentGuideSettingsContent {
+    /// Whether to display indent guides in the editor.
+    ///
+    /// Default: true
+    pub enabled: Option<bool>,
+    /// The width of the indent guides in pixels, between 1 and 10.
+    ///
+    /// Default: 1
+    pub line_width: Option<u32>,
+    /// The width of the active indent guide in pixels, between 1 and 10.
+    ///
+    /// Default: 1
+    pub active_line_width: Option<u32>,
+    /// Determines how indent guides are colored.
+    ///
+    /// Default: Fixed
+    pub coloring: Option<IndentGuideColoring>,
+    /// Determines how indent guide backgrounds are colored.
+    ///
+    /// Default: Disabled
+    pub background_coloring: Option<IndentGuideBackgroundColoring>,
+}
+
+/// The task settings for a particular language.
+#[skip_serializing_none]
+#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)]
+pub struct LanguageTaskSettingsContent {
+    /// Extra task variables to set for a particular language.
+    #[serde(default)]
+    pub variables: HashMap<String, String>,
+    pub enabled: Option<bool>,
+    /// Use LSP tasks over Zed language extension ones.
+    /// If no LSP tasks are returned due to error/timeout or regular execution,
+    /// Zed language extension tasks will be used instead.
+    ///
+    /// Other Zed tasks will still be shown:
+    /// * Zed task from either of the task config file
+    /// * Zed task from history (e.g. one-off task was spawned before)
+    pub prefer_lsp: Option<bool>,
+}
+
+/// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language
+/// names in the keys.
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct LanguageToSettingsMap(pub HashMap<SharedString, LanguageSettingsContent>);
+
+inventory::submit! {
+    ParameterizedJsonSchema {
+        add_and_get_ref: |generator, params, _cx| {
+            let language_settings_content_ref = generator
+                .subschema_for::<LanguageSettingsContent>()
+                .to_value();
+            replace_subschema::<LanguageToSettingsMap>(generator, || json_schema!({
+                "type": "object",
+                "properties": params
+                    .language_names
+                    .iter()
+                    .map(|name| {
+                        (
+                            name.clone(),
+                            language_settings_content_ref.clone(),
+                        )
+                    })
+                    .collect::<serde_json::Map<_, _>>()
+            }))
+        }
+    }
+}
+
+/// Determines how indent guides are colored.
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum IndentGuideColoring {
+    /// Do not render any lines for indent guides.
+    Disabled,
+    /// Use the same color for all indentation levels.
+    #[default]
+    Fixed,
+    /// Use a different color for each indentation level.
+    IndentAware,
+}
+
+/// Determines how indent guide backgrounds are colored.
+#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum IndentGuideBackgroundColoring {
+    /// Do not render any background for indent guides.
+    #[default]
+    Disabled,
+    /// Use a different color for each indentation level.
+    IndentAware,
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn test_formatter_deserialization() {
+        let raw_auto = "{\"formatter\": \"auto\"}";
+        let settings: LanguageSettingsContent = serde_json::from_str(raw_auto).unwrap();
+        assert_eq!(settings.formatter, Some(SelectedFormatter::Auto));
+        let raw = "{\"formatter\": \"language_server\"}";
+        let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
+        assert_eq!(
+            settings.formatter,
+            Some(SelectedFormatter::List(FormatterList::Single(
+                Formatter::LanguageServer { name: None }
+            )))
+        );
+        let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}";
+        let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
+        assert_eq!(
+            settings.formatter,
+            Some(SelectedFormatter::List(FormatterList::Vec(vec![
+                Formatter::LanguageServer { name: None }
+            ])))
+        );
+        let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"prettier\"]}";
+        let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
+        assert_eq!(
+            settings.formatter,
+            Some(SelectedFormatter::List(FormatterList::Vec(vec![
+                Formatter::LanguageServer { name: None },
+                Formatter::Prettier
+            ])))
+        );
+    }
+
+    #[test]
+    fn test_formatter_deserialization_invalid() {
+        let raw_auto = "{\"formatter\": {}}";
+        let result: Result<LanguageSettingsContent, _> = serde_json::from_str(raw_auto);
+        assert!(result.is_err());
+    }
+}

crates/settings/src/settings_content/language_model.rs πŸ”—

@@ -0,0 +1,419 @@
+use collections::HashMap;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+
+use std::sync::Arc;
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct AllLanguageModelSettingsContent {
+    pub anthropic: Option<AnthropicSettingsContent>,
+    pub bedrock: Option<AmazonBedrockSettingsContent>,
+    pub deepseek: Option<DeepseekSettingsContent>,
+    pub google: Option<GoogleSettingsContent>,
+    pub lmstudio: Option<LmStudioSettingsContent>,
+    pub mistral: Option<MistralSettingsContent>,
+    pub ollama: Option<OllamaSettingsContent>,
+    pub open_router: Option<OpenRouterSettingsContent>,
+    pub openai: Option<OpenAiSettingsContent>,
+    pub openai_compatible: Option<HashMap<Arc<str>, OpenAiCompatibleSettingsContent>>,
+    pub vercel: Option<VercelSettingsContent>,
+    pub x_ai: Option<XAiSettingsContent>,
+    #[serde(rename = "zed.dev")]
+    pub zed_dot_dev: Option<ZedDotDevSettingsContent>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct AnthropicSettingsContent {
+    pub api_url: Option<String>,
+    pub available_models: Option<Vec<AnthropicAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct AnthropicAvailableModel {
+    /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc
+    pub name: String,
+    /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel.
+    pub display_name: Option<String>,
+    /// The model's context window size.
+    pub max_tokens: u64,
+    /// A model `name` to substitute when calling tools, in case the primary model doesn't support tool calling.
+    pub tool_override: Option<String>,
+    /// Configuration of Anthropic's caching API.
+    pub cache_configuration: Option<LanguageModelCacheConfiguration>,
+    pub max_output_tokens: Option<u64>,
+    pub default_temperature: Option<f32>,
+    #[serde(default)]
+    pub extra_beta_headers: Vec<String>,
+    /// The model's mode (e.g. thinking)
+    pub mode: Option<ModelMode>,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct AmazonBedrockSettingsContent {
+    pub available_models: Option<Vec<BedrockAvailableModel>>,
+    pub endpoint_url: Option<String>,
+    pub region: Option<String>,
+    pub profile: Option<String>,
+    pub authentication_method: Option<BedrockAuthMethodContent>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct BedrockAvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+    pub cache_configuration: Option<LanguageModelCacheConfiguration>,
+    pub max_output_tokens: Option<u64>,
+    pub default_temperature: Option<f32>,
+    pub mode: Option<ModelMode>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub enum BedrockAuthMethodContent {
+    #[serde(rename = "named_profile")]
+    NamedProfile,
+    #[serde(rename = "sso")]
+    SingleSignOn,
+    /// IMDSv2, PodIdentity, env vars, etc.
+    #[serde(rename = "default")]
+    Automatic,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct OllamaSettingsContent {
+    pub api_url: Option<String>,
+    pub available_models: Option<Vec<OllamaAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct OllamaAvailableModel {
+    /// The model name in the Ollama API (e.g. "llama3.2:latest")
+    pub name: String,
+    /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel.
+    pub display_name: Option<String>,
+    /// The Context Length parameter to the model (aka num_ctx or n_ctx)
+    pub max_tokens: u64,
+    /// The number of seconds to keep the connection open after the last request
+    pub keep_alive: Option<KeepAlive>,
+    /// Whether the model supports tools
+    pub supports_tools: Option<bool>,
+    /// Whether the model supports vision
+    pub supports_images: Option<bool>,
+    /// Whether to enable think mode
+    pub supports_thinking: Option<bool>,
+}
+
+#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq, JsonSchema)]
+#[serde(untagged)]
+pub enum KeepAlive {
+    /// Keep model alive for N seconds
+    Seconds(isize),
+    /// Keep model alive for a fixed duration. Accepts durations like "5m", "10m", "1h", "1d", etc.
+    Duration(String),
+}
+
+impl KeepAlive {
+    /// Keep model alive until a new model is loaded or until Ollama shuts down
+    pub fn indefinite() -> Self {
+        Self::Seconds(-1)
+    }
+}
+
+impl Default for KeepAlive {
+    fn default() -> Self {
+        Self::indefinite()
+    }
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct LmStudioSettingsContent {
+    pub api_url: Option<String>,
+    pub available_models: Option<Vec<LmStudioAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct LmStudioAvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+    pub supports_tool_calls: bool,
+    pub supports_images: bool,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct DeepseekSettingsContent {
+    pub api_url: Option<String>,
+    pub available_models: Option<Vec<DeepseekAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct DeepseekAvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+    pub max_output_tokens: Option<u64>,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct MistralSettingsContent {
+    pub api_url: Option<String>,
+    pub available_models: Option<Vec<MistralAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct MistralAvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+    pub max_output_tokens: Option<u64>,
+    pub max_completion_tokens: Option<u64>,
+    pub supports_tools: Option<bool>,
+    pub supports_images: Option<bool>,
+    pub supports_thinking: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct OpenAiSettingsContent {
+    pub api_url: Option<String>,
+    pub available_models: Option<Vec<OpenAiAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct OpenAiAvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+    pub max_output_tokens: Option<u64>,
+    pub max_completion_tokens: Option<u64>,
+    pub reasoning_effort: Option<OpenAiReasoningEffort>,
+}
+
+#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum OpenAiReasoningEffort {
+    Minimal,
+    Low,
+    Medium,
+    High,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct OpenAiCompatibleSettingsContent {
+    pub api_url: String,
+    pub available_models: Vec<OpenAiCompatibleAvailableModel>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct OpenAiCompatibleAvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+    pub max_output_tokens: Option<u64>,
+    pub max_completion_tokens: Option<u64>,
+    #[serde(default)]
+    pub capabilities: OpenAiCompatibleModelCapabilities,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct OpenAiCompatibleModelCapabilities {
+    pub tools: bool,
+    pub images: bool,
+    pub parallel_tool_calls: bool,
+    pub prompt_cache_key: bool,
+}
+
+impl Default for OpenAiCompatibleModelCapabilities {
+    fn default() -> Self {
+        Self {
+            tools: true,
+            images: false,
+            parallel_tool_calls: false,
+            prompt_cache_key: false,
+        }
+    }
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct VercelSettingsContent {
+    pub api_url: Option<String>,
+    pub available_models: Option<Vec<VercelAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct VercelAvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+    pub max_output_tokens: Option<u64>,
+    pub max_completion_tokens: Option<u64>,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct GoogleSettingsContent {
+    pub api_url: Option<String>,
+    pub available_models: Option<Vec<GoogleAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct GoogleAvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+    pub mode: Option<ModelMode>,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct XAiSettingsContent {
+    pub api_url: Option<String>,
+    pub available_models: Option<Vec<XaiAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct XaiAvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+    pub max_output_tokens: Option<u64>,
+    pub max_completion_tokens: Option<u64>,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct ZedDotDevSettingsContent {
+    pub available_models: Option<Vec<ZedDotDevAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct ZedDotDevAvailableModel {
+    /// The provider of the language model.
+    pub provider: ZedDotDevAvailableProvider,
+    /// The model's name in the provider's API. e.g. claude-3-5-sonnet-20240620
+    pub name: String,
+    /// The name displayed in the UI, such as in the assistant panel model dropdown menu.
+    pub display_name: Option<String>,
+    /// The size of the context window, indicating the maximum number of tokens the model can process.
+    pub max_tokens: usize,
+    /// The maximum number of output tokens allowed by the model.
+    pub max_output_tokens: Option<u64>,
+    /// The maximum number of completion tokens allowed by the model (o1-* only)
+    pub max_completion_tokens: Option<u64>,
+    /// Override this model with a different Anthropic model for tool calls.
+    pub tool_override: Option<String>,
+    /// Indicates whether this custom model supports caching.
+    pub cache_configuration: Option<LanguageModelCacheConfiguration>,
+    /// The default temperature to use for this model.
+    pub default_temperature: Option<f32>,
+    /// Any extra beta headers to provide when using the model.
+    #[serde(default)]
+    pub extra_beta_headers: Vec<String>,
+    /// The model's mode (e.g. thinking)
+    pub mode: Option<ModelMode>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum ZedDotDevAvailableProvider {
+    Anthropic,
+    OpenAi,
+    Google,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+pub struct OpenRouterSettingsContent {
+    pub api_url: Option<String>,
+    pub available_models: Option<Vec<OpenRouterAvailableModel>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct OpenRouterAvailableModel {
+    pub name: String,
+    pub display_name: Option<String>,
+    pub max_tokens: u64,
+    pub max_output_tokens: Option<u64>,
+    pub max_completion_tokens: Option<u64>,
+    pub supports_tools: Option<bool>,
+    pub supports_images: Option<bool>,
+    pub mode: Option<ModelMode>,
+    pub provider: Option<OpenRouterProvider>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct OpenRouterProvider {
+    order: Option<Vec<String>>,
+    #[serde(default = "default_true")]
+    allow_fallbacks: bool,
+    #[serde(default)]
+    require_parameters: bool,
+    #[serde(default)]
+    data_collection: DataCollection,
+    only: Option<Vec<String>>,
+    ignore: Option<Vec<String>>,
+    quantizations: Option<Vec<String>>,
+    sort: Option<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum DataCollection {
+    Allow,
+    Disallow,
+}
+
+impl Default for DataCollection {
+    fn default() -> Self {
+        Self::Allow
+    }
+}
+
+fn default_true() -> bool {
+    true
+}
+
+/// Configuration for caching language model messages.
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct LanguageModelCacheConfiguration {
+    pub max_cache_anchors: usize,
+    pub should_speculate: bool,
+    pub min_total_token: u64,
+}
+
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(tag = "type", rename_all = "lowercase")]
+pub enum ModelMode {
+    #[default]
+    Default,
+    Thinking {
+        /// The maximum number of tokens to use for reasoning. Must be lower than the model's `max_output_tokens`.
+        budget_tokens: Option<u32>,
+    },
+}

crates/settings/src/settings_content/project.rs πŸ”—

@@ -0,0 +1,435 @@
+use std::{path::PathBuf, sync::Arc};
+
+use collections::{BTreeMap, HashMap};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+use util::serde::default_true;
+
+use crate::{AllLanguageSettingsContent, SlashCommandSettings};
+
+#[skip_serializing_none]
+#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ProjectSettingsContent {
+    #[serde(flatten)]
+    pub all_languages: AllLanguageSettingsContent,
+
+    #[serde(flatten)]
+    pub worktree: WorktreeSettingsContent,
+
+    /// Configuration for language servers.
+    ///
+    /// The following settings can be overridden for specific language servers:
+    /// - initialization_options
+    ///
+    /// To override settings for a language, add an entry for that language server's
+    /// name to the lsp value.
+    /// Default: null
+    #[serde(default)]
+    pub lsp: HashMap<Arc<str>, LspSettings>,
+
+    /// Configuration for Debugger-related features
+    #[serde(default)]
+    pub dap: HashMap<Arc<str>, DapSettingsContent>,
+
+    /// Settings for context servers used for AI-related features.
+    #[serde(default)]
+    pub context_servers: HashMap<Arc<str>, ContextServerSettingsContent>,
+
+    /// Configuration for how direnv configuration should be loaded
+    pub load_direnv: Option<DirenvSettings>,
+
+    /// Settings for slash commands.
+    pub slash_commands: Option<SlashCommandSettings>,
+
+    /// The list of custom Git hosting providers.
+    pub git_hosting_providers: Option<Vec<GitHostingProviderConfig>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct WorktreeSettingsContent {
+    /// The displayed name of this project. If not set, the root directory name
+    /// will be displayed.
+    ///
+    /// Default: none
+    pub project_name: Option<String>,
+
+    /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
+    /// `file_scan_inclusions`.
+    ///
+    /// Default: [
+    ///   "**/.git",
+    ///   "**/.svn",
+    ///   "**/.hg",
+    ///   "**/.jj",
+    ///   "**/CVS",
+    ///   "**/.DS_Store",
+    ///   "**/Thumbs.db",
+    ///   "**/.classpath",
+    ///   "**/.settings"
+    /// ]
+    pub file_scan_exclusions: Option<Vec<String>>,
+
+    /// Always include files that match these globs when scanning for files, even if they're
+    /// ignored by git. This setting is overridden by `file_scan_exclusions`.
+    /// Default: [
+    ///  ".env*",
+    ///  "docker-compose.*.yml",
+    /// ]
+    pub file_scan_inclusions: Option<Vec<String>>,
+
+    /// Treat the files matching these globs as `.env` files.
+    /// Default: ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"]
+    pub private_files: Option<Vec<String>>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
+#[serde(rename_all = "snake_case")]
+pub struct LspSettings {
+    pub binary: Option<BinarySettings>,
+    pub initialization_options: Option<serde_json::Value>,
+    pub settings: Option<serde_json::Value>,
+    /// If the server supports sending tasks over LSP extensions,
+    /// this setting can be used to enable or disable them in Zed.
+    /// Default: true
+    #[serde(default = "default_true")]
+    pub enable_lsp_tasks: bool,
+    pub fetch: Option<FetchSettings>,
+}
+
+impl Default for LspSettings {
+    fn default() -> Self {
+        Self {
+            binary: None,
+            initialization_options: None,
+            settings: None,
+            enable_lsp_tasks: true,
+            fetch: None,
+        }
+    }
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
+pub struct BinarySettings {
+    pub path: Option<String>,
+    pub arguments: Option<Vec<String>>,
+    pub env: Option<BTreeMap<String, String>>,
+    pub ignore_system_version: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
+pub struct FetchSettings {
+    // Whether to consider pre-releases for fetching
+    pub pre_release: Option<bool>,
+}
+
+/// Common language server settings.
+#[skip_serializing_none]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct GlobalLspSettingsContent {
+    /// Whether to show the LSP servers button in the status bar.
+    ///
+    /// Default: `true`
+    pub button: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct DapSettingsContent {
+    pub binary: Option<String>,
+    #[serde(default)]
+    pub args: Option<Vec<String>>,
+}
+
+#[skip_serializing_none]
+#[derive(Default, Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema)]
+pub struct SessionSettingsContent {
+    /// Whether or not to restore unsaved buffers on restart.
+    ///
+    /// If this is true, user won't be prompted whether to save/discard
+    /// dirty files when closing the application.
+    ///
+    /// Default: true
+    pub restore_unsaved_buffers: Option<bool>,
+}
+
+#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
+#[serde(tag = "source", rename_all = "snake_case")]
+pub enum ContextServerSettingsContent {
+    Custom {
+        /// Whether the context server is enabled.
+        #[serde(default = "default_true")]
+        enabled: bool,
+
+        #[serde(flatten)]
+        command: ContextServerCommand,
+    },
+    Extension {
+        /// Whether the context server is enabled.
+        #[serde(default = "default_true")]
+        enabled: bool,
+        /// The settings for this context server specified by the extension.
+        ///
+        /// Consult the documentation for the context server to see what settings
+        /// are supported.
+        settings: serde_json::Value,
+    },
+}
+impl ContextServerSettingsContent {
+    pub fn set_enabled(&mut self, enabled: bool) {
+        match self {
+            ContextServerSettingsContent::Custom {
+                enabled: custom_enabled,
+                command: _,
+            } => {
+                *custom_enabled = enabled;
+            }
+            ContextServerSettingsContent::Extension {
+                enabled: ext_enabled,
+                settings: _,
+            } => *ext_enabled = enabled,
+        }
+    }
+}
+
+#[skip_serializing_none]
+#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
+pub struct ContextServerCommand {
+    #[serde(rename = "command")]
+    pub path: PathBuf,
+    pub args: Vec<String>,
+    pub env: Option<HashMap<String, String>>,
+    /// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified.
+    pub timeout: Option<u64>,
+}
+
+impl std::fmt::Debug for ContextServerCommand {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let filtered_env = self.env.as_ref().map(|env| {
+            env.iter()
+                .map(|(k, v)| {
+                    (
+                        k,
+                        if util::redact::should_redact(k) {
+                            "[REDACTED]"
+                        } else {
+                            v
+                        },
+                    )
+                })
+                .collect::<Vec<_>>()
+        });
+
+        f.debug_struct("ContextServerCommand")
+            .field("path", &self.path)
+            .field("args", &self.args)
+            .field("env", &filtered_env)
+            .finish()
+    }
+}
+
+#[skip_serializing_none]
+#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+pub struct GitSettings {
+    /// Whether or not to show the git gutter.
+    ///
+    /// Default: tracked_files
+    pub git_gutter: Option<GitGutterSetting>,
+    /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter.
+    ///
+    /// Default: null
+    pub gutter_debounce: Option<u64>,
+    /// Whether or not to show git blame data inline in
+    /// the currently focused line.
+    ///
+    /// Default: on
+    pub inline_blame: Option<InlineBlameSettings>,
+    /// Which information to show in the branch picker.
+    ///
+    /// Default: on
+    pub branch_picker: Option<BranchPickerSettingsContent>,
+    /// How hunks are displayed visually in the editor.
+    ///
+    /// Default: staged_hollow
+    pub hunk_style: Option<GitHunkStyleSetting>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitGutterSetting {
+    /// Show git gutter in tracked files.
+    #[default]
+    TrackedFiles,
+    /// Hide git gutter
+    Hide,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct InlineBlameSettings {
+    /// Whether or not to show git blame data inline in
+    /// the currently focused line.
+    ///
+    /// Default: true
+    pub enabled: Option<bool>,
+    /// Whether to only show the inline blame information
+    /// after a delay once the cursor stops moving.
+    ///
+    /// Default: 0
+    pub delay_ms: Option<u64>,
+    /// The amount of padding between the end of the source line and the start
+    /// of the inline blame in units of columns.
+    ///
+    /// Default: 7
+    pub padding: Option<u32>,
+    /// The minimum column number to show the inline blame information at
+    ///
+    /// Default: 0
+    pub min_column: Option<u32>,
+    /// Whether to show commit summary as part of the inline blame.
+    ///
+    /// Default: false
+    pub show_commit_summary: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct BranchPickerSettingsContent {
+    /// Whether to show author name as part of the commit information.
+    ///
+    /// Default: false
+    pub show_author_name: Option<bool>,
+}
+
+#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitHunkStyleSetting {
+    /// Show unstaged hunks with a filled background and staged hunks hollow.
+    #[default]
+    StagedHollow,
+    /// Show unstaged hunks hollow and staged hunks with a filled background.
+    UnstagedHollow,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct DiagnosticsSettingsContent {
+    /// Whether to show the project diagnostics button in the status bar.
+    pub button: Option<bool>,
+
+    /// Whether or not to include warning diagnostics.
+    pub include_warnings: Option<bool>,
+
+    /// Settings for using LSP pull diagnostics mechanism in Zed.
+    pub lsp_pull_diagnostics: Option<LspPullDiagnosticsSettingsContent>,
+
+    /// Settings for showing inline diagnostics.
+    pub inline: Option<InlineDiagnosticsSettingsContent>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct LspPullDiagnosticsSettingsContent {
+    /// Whether to pull for diagnostics or not.
+    ///
+    /// Default: true
+    pub enabled: Option<bool>,
+    /// Minimum time to wait before pulling diagnostics from the language server(s).
+    /// 0 turns the debounce off.
+    ///
+    /// Default: 50
+    pub debounce_ms: Option<u64>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize, JsonSchema, Eq)]
+pub struct InlineDiagnosticsSettingsContent {
+    /// Whether or not to show inline diagnostics
+    ///
+    /// Default: false
+    pub enabled: Option<bool>,
+    /// Whether to only show the inline diagnostics after a delay after the
+    /// last editor event.
+    ///
+    /// Default: 150
+    pub update_debounce_ms: Option<u64>,
+    /// The amount of padding between the end of the source line and the start
+    /// of the inline diagnostic in units of columns.
+    ///
+    /// Default: 4
+    pub padding: Option<u32>,
+    /// The minimum column to display inline diagnostics. This setting can be
+    /// used to horizontally align inline diagnostics at some position. Lines
+    /// longer than this value will still push diagnostics further to the right.
+    ///
+    /// Default: 0
+    pub min_column: Option<u32>,
+
+    pub max_severity: Option<DiagnosticSeverityContent>,
+}
+
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+pub struct NodeBinarySettings {
+    /// The path to the Node binary.
+    pub path: Option<String>,
+    /// The path to the npm binary Zed should use (defaults to `.path/../npm`).
+    pub npm_path: Option<String>,
+    /// If enabled, Zed will download its own copy of Node.
+    pub ignore_system_version: Option<bool>,
+}
+
+#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DirenvSettings {
+    /// Load direnv configuration through a shell hook
+    ShellHook,
+    /// Load direnv configuration directly using `direnv export json`
+    #[default]
+    Direct,
+}
+
+#[derive(
+    Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum DiagnosticSeverityContent {
+    // No diagnostics are shown.
+    Off,
+    Error,
+    Warning,
+    Info,
+    #[serde(alias = "all")]
+    Hint,
+}
+
+/// A custom Git hosting provider.
+#[skip_serializing_none]
+#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct GitHostingProviderConfig {
+    /// The type of the provider.
+    ///
+    /// Must be one of `github`, `gitlab`, or `bitbucket`.
+    pub provider: GitHostingProviderKind,
+
+    /// The base URL for the provider (e.g., "https://code.corp.big.com").
+    pub base_url: String,
+
+    /// The display name for the provider (e.g., "BigCorp GitHub").
+    pub name: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitHostingProviderKind {
+    Github,
+    Gitlab,
+    Bitbucket,
+}

crates/settings/src/settings_content/terminal.rs πŸ”—

@@ -0,0 +1,318 @@
+use std::path::PathBuf;
+
+use collections::HashMap;
+use gpui::{AbsoluteLength, FontFeatures, SharedString, px};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+
+use crate::FontFamilyName;
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+pub struct TerminalSettingsContent {
+    /// What shell to use when opening a terminal.
+    ///
+    /// Default: system
+    pub shell: Option<Shell>,
+    /// What working directory to use when launching the terminal
+    ///
+    /// Default: current_project_directory
+    pub working_directory: Option<WorkingDirectory>,
+    /// Sets the terminal's font size.
+    ///
+    /// If this option is not included,
+    /// the terminal will default to matching the buffer's font size.
+    pub font_size: Option<f32>,
+    /// Sets the terminal's font family.
+    ///
+    /// If this option is not included,
+    /// the terminal will default to matching the buffer's font family.
+    pub font_family: Option<FontFamilyName>,
+
+    /// Sets the terminal's font fallbacks.
+    ///
+    /// If this option is not included,
+    /// the terminal will default to matching the buffer's font fallbacks.
+    #[schemars(extend("uniqueItems" = true))]
+    pub font_fallbacks: Option<Vec<FontFamilyName>>,
+
+    /// Sets the terminal's line height.
+    ///
+    /// Default: comfortable
+    pub line_height: Option<TerminalLineHeight>,
+    pub font_features: Option<FontFeatures>,
+    /// Sets the terminal's font weight in CSS weight units 0-900.
+    pub font_weight: Option<f32>,
+    /// Any key-value pairs added to this list will be added to the terminal's
+    /// environment. Use `:` to separate multiple values.
+    ///
+    /// Default: {}
+    pub env: Option<HashMap<String, String>>,
+    /// Default cursor shape for the terminal.
+    /// Can be "bar", "block", "underline", or "hollow".
+    ///
+    /// Default: None
+    pub cursor_shape: Option<CursorShapeContent>,
+    /// Sets the cursor blinking behavior in the terminal.
+    ///
+    /// Default: terminal_controlled
+    pub blinking: Option<TerminalBlink>,
+    /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
+    /// Alternate Scroll mode converts mouse scroll events into up / down key
+    /// presses when in the alternate screen (e.g. when running applications
+    /// like vim or  less). The terminal can still set and unset this mode.
+    ///
+    /// Default: on
+    pub alternate_scroll: Option<AlternateScroll>,
+    /// Sets whether the option key behaves as the meta key.
+    ///
+    /// Default: false
+    pub option_as_meta: Option<bool>,
+    /// Whether or not selecting text in the terminal will automatically
+    /// copy to the system clipboard.
+    ///
+    /// Default: false
+    pub copy_on_select: Option<bool>,
+    /// Whether to keep the text selection after copying it to the clipboard.
+    ///
+    /// Default: false
+    pub keep_selection_on_copy: Option<bool>,
+    /// Whether to show the terminal button in the status bar.
+    ///
+    /// Default: true
+    pub button: Option<bool>,
+    pub dock: Option<TerminalDockPosition>,
+    /// Default width when the terminal is docked to the left or right.
+    ///
+    /// Default: 640
+    pub default_width: Option<f32>,
+    /// Default height when the terminal is docked to the bottom.
+    ///
+    /// Default: 320
+    pub default_height: Option<f32>,
+    /// Activates the python virtual environment, if one is found, in the
+    /// terminal's working directory (as resolved by the working_directory
+    /// setting). Set this to "off" to disable this behavior.
+    ///
+    /// Default: on
+    pub detect_venv: Option<VenvSettings>,
+    /// The maximum number of lines to keep in the scrollback history.
+    /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
+    /// 0 disables the scrolling.
+    /// Existing terminals will not pick up this change until they are recreated.
+    /// See <a href="https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213">Alacritty documentation</a> for more information.
+    ///
+    /// Default: 10_000
+    pub max_scroll_history_lines: Option<usize>,
+    /// Toolbar related settings
+    pub toolbar: Option<TerminalToolbarContent>,
+    /// Scrollbar-related settings
+    pub scrollbar: Option<ScrollbarSettingsContent>,
+    /// The minimum APCA perceptual contrast between foreground and background colors.
+    ///
+    /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
+    /// especially for dark mode. Values range from 0 to 106.
+    ///
+    /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
+    /// https://readtech.org/ARC/tests/bronze-simple-mode/
+    /// - 0: No contrast adjustment
+    /// - 45: Minimum for large fluent text (36px+)
+    /// - 60: Minimum for other content text
+    /// - 75: Minimum for body text
+    /// - 90: Preferred for body text
+    ///
+    /// Default: 45
+    pub minimum_contrast: Option<f32>,
+}
+
+/// Shell configuration to open the terminal with.
+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Shell {
+    /// Use the system's default terminal configuration in /etc/passwd
+    #[default]
+    System,
+    /// Use a specific program with no arguments.
+    Program(String),
+    /// Use a specific program with arguments.
+    WithArguments {
+        /// The program to run.
+        program: String,
+        /// The arguments to pass to the program.
+        args: Vec<String>,
+        /// An optional string to override the title of the terminal tab
+        title_override: Option<SharedString>,
+    },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum WorkingDirectory {
+    /// Use the current file's project directory.  Will Fallback to the
+    /// first project directory strategy if unsuccessful.
+    CurrentProjectDirectory,
+    /// Use the first project in this workspace's directory.
+    FirstProjectDirectory,
+    /// Always use this platform's home directory (if it can be found).
+    AlwaysHome,
+    /// Always use a specific directory. This value will be shell expanded.
+    /// If this path is not a valid directory the terminal will default to
+    /// this platform's home directory  (if it can be found).
+    Always { directory: String },
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ScrollbarSettingsContent {
+    /// When to show the scrollbar in the terminal.
+    ///
+    /// Default: inherits editor scrollbar settings
+    pub show: Option<Option<ShowScrollbar>>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalLineHeight {
+    /// Use a line height that's comfortable for reading, 1.618
+    #[default]
+    Comfortable,
+    /// Use a standard line height, 1.3. This option is useful for TUIs,
+    /// particularly if they use box characters
+    Standard,
+    /// Use a custom line height.
+    Custom(f32),
+}
+
+impl TerminalLineHeight {
+    pub fn value(&self) -> AbsoluteLength {
+        let value = match self {
+            TerminalLineHeight::Comfortable => 1.618,
+            TerminalLineHeight::Standard => 1.3,
+            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
+        };
+        px(value).into()
+    }
+}
+
+/// When to show the scrollbar.
+///
+/// Default: auto
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowScrollbar {
+    /// Show the scrollbar if there's important information or
+    /// follow the system's configured behavior.
+    Auto,
+    /// Match the system's configured behavior.
+    System,
+    /// Always show the scrollbar.
+    Always,
+    /// Never show the scrollbar.
+    Never,
+}
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum CursorShapeContent {
+    /// Cursor is a block like `β–ˆ`.
+    #[default]
+    Block,
+    /// Cursor is an underscore like `_`.
+    Underline,
+    /// Cursor is a vertical bar like `⎸`.
+    Bar,
+    /// Cursor is a hollow box like `β–―`.
+    Hollow,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalBlink {
+    /// Never blink the cursor, ignoring the terminal mode.
+    Off,
+    /// Default the cursor blink to off, but allow the terminal to
+    /// set blinking.
+    TerminalControlled,
+    /// Always blink the cursor, ignoring the terminal mode.
+    On,
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum AlternateScroll {
+    On,
+    Off,
+}
+
+// Toolbar related settings
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct TerminalToolbarContent {
+    /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
+    /// Only shown if the terminal title is not empty.
+    ///
+    /// The shell running in the terminal needs to be configured to emit the title.
+    /// Example: `echo -e "\e]2;New Title\007";`
+    ///
+    /// Default: true
+    pub breadcrumbs: Option<bool>,
+}
+
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum VenvSettings {
+    #[default]
+    Off,
+    On {
+        /// Default directories to search for virtual environments, relative
+        /// to the current working directory. We recommend overriding this
+        /// in your project's settings, rather than globally.
+        activate_script: Option<ActivateScript>,
+        venv_name: Option<String>,
+        directories: Option<Vec<PathBuf>>,
+    },
+}
+#[skip_serializing_none]
+pub struct VenvSettingsContent<'a> {
+    pub activate_script: ActivateScript,
+    pub venv_name: &'a str,
+    pub directories: &'a [PathBuf],
+}
+
+impl VenvSettings {
+    pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
+        match self {
+            VenvSettings::Off => None,
+            VenvSettings::On {
+                activate_script,
+                venv_name,
+                directories,
+            } => Some(VenvSettingsContent {
+                activate_script: activate_script.unwrap_or(ActivateScript::Default),
+                venv_name: venv_name.as_deref().unwrap_or(""),
+                directories: directories.as_deref().unwrap_or(&[]),
+            }),
+        }
+    }
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalDockPosition {
+    Left,
+    Bottom,
+    Right,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ActivateScript {
+    #[default]
+    Default,
+    Csh,
+    Fish,
+    Nushell,
+    PowerShell,
+    Pyenv,
+}

crates/settings/src/settings_content/theme.rs πŸ”—

@@ -0,0 +1,1076 @@
+use collections::{HashMap, IndexMap};
+use gpui::{FontFallbacks, FontFeatures, FontStyle, FontWeight};
+use schemars::{JsonSchema, JsonSchema_repr};
+use serde::{Deserialize, Deserializer, Serialize};
+use serde_json::Value;
+use serde_repr::{Deserialize_repr, Serialize_repr};
+use std::sync::Arc;
+
+use serde_with::skip_serializing_none;
+
+/// Settings for rendering text in UI and text buffers.
+
+#[skip_serializing_none]
+#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ThemeSettingsContent {
+    /// The default font size for text in the UI.
+    #[serde(default)]
+    pub ui_font_size: Option<f32>,
+    /// The name of a font to use for rendering in the UI.
+    #[serde(default)]
+    pub ui_font_family: Option<FontFamilyName>,
+    /// The font fallbacks to use for rendering in the UI.
+    #[serde(default)]
+    #[schemars(default = "default_font_fallbacks")]
+    #[schemars(extend("uniqueItems" = true))]
+    pub ui_font_fallbacks: Option<Vec<FontFamilyName>>,
+    /// The OpenType features to enable for text in the UI.
+    #[serde(default)]
+    #[schemars(default = "default_font_features")]
+    pub ui_font_features: Option<FontFeatures>,
+    /// The weight of the UI font in CSS units from 100 to 900.
+    #[serde(default)]
+    pub ui_font_weight: Option<f32>,
+    /// The name of a font to use for rendering in text buffers.
+    #[serde(default)]
+    pub buffer_font_family: Option<FontFamilyName>,
+    /// The font fallbacks to use for rendering in text buffers.
+    #[serde(default)]
+    #[schemars(extend("uniqueItems" = true))]
+    pub buffer_font_fallbacks: Option<Vec<FontFamilyName>>,
+    /// The default font size for rendering in text buffers.
+    #[serde(default)]
+    pub buffer_font_size: Option<f32>,
+    /// The weight of the editor font in CSS units from 100 to 900.
+    #[serde(default)]
+    pub buffer_font_weight: Option<f32>,
+    /// The buffer's line height.
+    #[serde(default)]
+    pub buffer_line_height: Option<BufferLineHeight>,
+    /// The OpenType features to enable for rendering in text buffers.
+    #[serde(default)]
+    #[schemars(default = "default_font_features")]
+    pub buffer_font_features: Option<FontFeatures>,
+    /// The font size for the agent panel. Falls back to the UI font size if unset.
+    #[serde(default)]
+    pub agent_font_size: Option<Option<f32>>,
+    /// The name of the Zed theme to use.
+    #[serde(default)]
+    pub theme: Option<ThemeSelection>,
+    /// The name of the icon theme to use.
+    #[serde(default)]
+    pub icon_theme: Option<IconThemeSelection>,
+
+    /// UNSTABLE: Expect many elements to be broken.
+    ///
+    // Controls the density of the UI.
+    #[serde(rename = "unstable.ui_density", default)]
+    pub ui_density: Option<UiDensity>,
+
+    /// How much to fade out unused code.
+    #[serde(default)]
+    pub unnecessary_code_fade: Option<f32>,
+
+    /// EXPERIMENTAL: Overrides for the current theme.
+    ///
+    /// These values will override the ones on the current theme specified in `theme`.
+    #[serde(rename = "experimental.theme_overrides", default)]
+    pub experimental_theme_overrides: Option<ThemeStyleContent>,
+
+    /// Overrides per theme
+    ///
+    /// These values will override the ones on the specified theme
+    #[serde(default)]
+    pub theme_overrides: HashMap<String, ThemeStyleContent>,
+}
+
+fn default_font_features() -> Option<FontFeatures> {
+    Some(FontFeatures::default())
+}
+
+fn default_font_fallbacks() -> Option<FontFallbacks> {
+    Some(FontFallbacks::default())
+}
+
+/// Represents the selection of a theme, which can be either static or dynamic.
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(untagged)]
+pub enum ThemeSelection {
+    /// A static theme selection, represented by a single theme name.
+    Static(ThemeName),
+    /// A dynamic theme selection, which can change based the [ThemeMode].
+    Dynamic {
+        /// The mode used to determine which theme to use.
+        #[serde(default)]
+        mode: ThemeMode,
+        /// The theme to use for light mode.
+        light: ThemeName,
+        /// The theme to use for dark mode.
+        dark: ThemeName,
+    },
+}
+
+/// Represents the selection of an icon theme, which can be either static or dynamic.
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(untagged)]
+pub enum IconThemeSelection {
+    /// A static icon theme selection, represented by a single icon theme name.
+    Static(IconThemeName),
+    /// A dynamic icon theme selection, which can change based on the [`ThemeMode`].
+    Dynamic {
+        /// The mode used to determine which theme to use.
+        #[serde(default)]
+        mode: ThemeMode,
+        /// The icon theme to use for light mode.
+        light: IconThemeName,
+        /// The icon theme to use for dark mode.
+        dark: IconThemeName,
+    },
+}
+
+// TODO: Rename ThemeMode -> ThemeAppearanceMode
+/// The mode use to select a theme.
+///
+/// `Light` and `Dark` will select their respective themes.
+///
+/// `System` will select the theme based on the system's appearance.
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ThemeMode {
+    /// Use the specified `light` theme.
+    Light,
+
+    /// Use the specified `dark` theme.
+    Dark,
+
+    /// Use the theme based on the system's appearance.
+    #[default]
+    System,
+}
+
+/// Specifies the density of the UI.
+/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
+#[derive(
+    Debug,
+    Default,
+    PartialEq,
+    Eq,
+    PartialOrd,
+    Ord,
+    Hash,
+    Clone,
+    Copy,
+    Serialize,
+    Deserialize,
+    JsonSchema,
+)]
+#[serde(rename_all = "snake_case")]
+pub enum UiDensity {
+    /// A denser UI with tighter spacing and smaller elements.
+    #[serde(alias = "compact")]
+    Compact,
+    #[default]
+    #[serde(alias = "default")]
+    /// The default UI density.
+    Default,
+    #[serde(alias = "comfortable")]
+    /// A looser UI with more spacing and larger elements.
+    Comfortable,
+}
+
+impl UiDensity {
+    /// The spacing ratio of a given density.
+    /// TODO: Standardize usage throughout the app or remove
+    pub fn spacing_ratio(self) -> f32 {
+        match self {
+            UiDensity::Compact => 0.75,
+            UiDensity::Default => 1.0,
+            UiDensity::Comfortable => 1.25,
+        }
+    }
+}
+
+/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at
+/// runtime.
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(transparent)]
+pub struct FontFamilyName(pub Arc<str>);
+
+/// The buffer's line height.
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum BufferLineHeight {
+    /// A less dense line height.
+    #[default]
+    Comfortable,
+    /// The default line height.
+    Standard,
+    /// A custom line height, where 1.0 is the font's height. Must be at least 1.0.
+    Custom(#[serde(deserialize_with = "deserialize_line_height")] f32),
+}
+
+fn deserialize_line_height<'de, D>(deserializer: D) -> Result<f32, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    let value = f32::deserialize(deserializer)?;
+    if value < 1.0 {
+        return Err(serde::de::Error::custom(
+            "buffer_line_height.custom must be at least 1.0",
+        ));
+    }
+
+    Ok(value)
+}
+
+/// The content of a serialized theme.
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[serde(default)]
+pub struct ThemeStyleContent {
+    #[serde(default, rename = "background.appearance")]
+    pub window_background_appearance: Option<WindowBackgroundContent>,
+
+    #[serde(default)]
+    pub accents: Vec<AccentContent>,
+
+    #[serde(flatten, default)]
+    pub colors: ThemeColorsContent,
+
+    #[serde(flatten, default)]
+    pub status: StatusColorsContent,
+
+    #[serde(default)]
+    pub players: Vec<PlayerColorContent>,
+
+    /// The styles for syntax nodes.
+    #[serde(default)]
+    pub syntax: IndexMap<String, HighlightStyleContent>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct AccentContent(pub Option<String>);
+
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
+pub struct PlayerColorContent {
+    pub cursor: Option<String>,
+    pub background: Option<String>,
+    pub selection: Option<String>,
+}
+
+/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime.
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(transparent)]
+pub struct ThemeName(pub Arc<str>);
+
+/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at
+/// runtime.
+#[skip_serializing_none]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(transparent)]
+pub struct IconThemeName(pub Arc<str>);
+
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[serde(default)]
+pub struct ThemeColorsContent {
+    /// Border color. Used for most borders, is usually a high contrast color.
+    #[serde(rename = "border")]
+    pub border: Option<String>,
+
+    /// Border color. Used for deemphasized borders, like a visual divider between two sections
+    #[serde(rename = "border.variant")]
+    pub border_variant: Option<String>,
+
+    /// Border color. Used for focused elements, like keyboard focused list item.
+    #[serde(rename = "border.focused")]
+    pub border_focused: Option<String>,
+
+    /// Border color. Used for selected elements, like an active search filter or selected checkbox.
+    #[serde(rename = "border.selected")]
+    pub border_selected: Option<String>,
+
+    /// Border color. Used for transparent borders. Used for placeholder borders when an element gains a border on state change.
+    #[serde(rename = "border.transparent")]
+    pub border_transparent: Option<String>,
+
+    /// Border color. Used for disabled elements, like a disabled input or button.
+    #[serde(rename = "border.disabled")]
+    pub border_disabled: Option<String>,
+
+    /// Background color. Used for elevated surfaces, like a context menu, popup, or dialog.
+    #[serde(rename = "elevated_surface.background")]
+    pub elevated_surface_background: Option<String>,
+
+    /// Background Color. Used for grounded surfaces like a panel or tab.
+    #[serde(rename = "surface.background")]
+    pub surface_background: Option<String>,
+
+    /// Background Color. Used for the app background and blank panels or windows.
+    #[serde(rename = "background")]
+    pub background: Option<String>,
+
+    /// Background Color. Used for the background of an element that should have a different background than the surface it's on.
+    ///
+    /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons...
+    ///
+    /// For an element that should have the same background as the surface it's on, use `ghost_element_background`.
+    #[serde(rename = "element.background")]
+    pub element_background: Option<String>,
+
+    /// Background Color. Used for the hover state of an element that should have a different background than the surface it's on.
+    ///
+    /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen.
+    #[serde(rename = "element.hover")]
+    pub element_hover: Option<String>,
+
+    /// Background Color. Used for the active state of an element that should have a different background than the surface it's on.
+    ///
+    /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed.
+    #[serde(rename = "element.active")]
+    pub element_active: Option<String>,
+
+    /// Background Color. Used for the selected state of an element that should have a different background than the surface it's on.
+    ///
+    /// Selected states are triggered by the element being selected (or "activated") by the user.
+    ///
+    /// This could include a selected checkbox, a toggleable button that is toggled on, etc.
+    #[serde(rename = "element.selected")]
+    pub element_selected: Option<String>,
+
+    /// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on.
+    ///
+    /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input.
+    #[serde(rename = "element.disabled")]
+    pub element_disabled: Option<String>,
+
+    /// Background Color. Used for the background of selections in a UI element.
+    #[serde(rename = "element.selection_background")]
+    pub element_selection_background: Option<String>,
+
+    /// Background Color. Used for the area that shows where a dragged element will be dropped.
+    #[serde(rename = "drop_target.background")]
+    pub drop_target_background: Option<String>,
+
+    /// Border Color. Used for the border that shows where a dragged element will be dropped.
+    #[serde(rename = "drop_target.border")]
+    pub drop_target_border: Option<String>,
+
+    /// Used for the background of a ghost element that should have the same background as the surface it's on.
+    ///
+    /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons...
+    ///
+    /// For an element that should have a different background than the surface it's on, use `element_background`.
+    #[serde(rename = "ghost_element.background")]
+    pub ghost_element_background: Option<String>,
+
+    /// Background Color. Used for the hover state of a ghost element that should have the same background as the surface it's on.
+    ///
+    /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen.
+    #[serde(rename = "ghost_element.hover")]
+    pub ghost_element_hover: Option<String>,
+
+    /// Background Color. Used for the active state of a ghost element that should have the same background as the surface it's on.
+    ///
+    /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed.
+    #[serde(rename = "ghost_element.active")]
+    pub ghost_element_active: Option<String>,
+
+    /// Background Color. Used for the selected state of a ghost element that should have the same background as the surface it's on.
+    ///
+    /// Selected states are triggered by the element being selected (or "activated") by the user.
+    ///
+    /// This could include a selected checkbox, a toggleable button that is toggled on, etc.
+    #[serde(rename = "ghost_element.selected")]
+    pub ghost_element_selected: Option<String>,
+
+    /// Background Color. Used for the disabled state of a ghost element that should have the same background as the surface it's on.
+    ///
+    /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input.
+    #[serde(rename = "ghost_element.disabled")]
+    pub ghost_element_disabled: Option<String>,
+
+    /// Text Color. Default text color used for most text.
+    #[serde(rename = "text")]
+    pub text: Option<String>,
+
+    /// Text Color. Color of muted or deemphasized text. It is a subdued version of the standard text color.
+    #[serde(rename = "text.muted")]
+    pub text_muted: Option<String>,
+
+    /// Text Color. Color of the placeholder text typically shown in input fields to guide the user to enter valid data.
+    #[serde(rename = "text.placeholder")]
+    pub text_placeholder: Option<String>,
+
+    /// Text Color. Color used for text denoting disabled elements. Typically, the color is faded or grayed out to emphasize the disabled state.
+    #[serde(rename = "text.disabled")]
+    pub text_disabled: Option<String>,
+
+    /// Text Color. Color used for emphasis or highlighting certain text, like an active filter or a matched character in a search.
+    #[serde(rename = "text.accent")]
+    pub text_accent: Option<String>,
+
+    /// Fill Color. Used for the default fill color of an icon.
+    #[serde(rename = "icon")]
+    pub icon: Option<String>,
+
+    /// Fill Color. Used for the muted or deemphasized fill color of an icon.
+    ///
+    /// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight.
+    #[serde(rename = "icon.muted")]
+    pub icon_muted: Option<String>,
+
+    /// Fill Color. Used for the disabled fill color of an icon.
+    ///
+    /// Disabled states are shown when a user cannot interact with an element, like a icon button.
+    #[serde(rename = "icon.disabled")]
+    pub icon_disabled: Option<String>,
+
+    /// Fill Color. Used for the placeholder fill color of an icon.
+    ///
+    /// This might be used to show an icon in an input that disappears when the user enters text.
+    #[serde(rename = "icon.placeholder")]
+    pub icon_placeholder: Option<String>,
+
+    /// Fill Color. Used for the accent fill color of an icon.
+    ///
+    /// This might be used to show when a toggleable icon button is selected.
+    #[serde(rename = "icon.accent")]
+    pub icon_accent: Option<String>,
+
+    /// Color used to accent some of the debuggers elements
+    /// Only accent breakpoint & breakpoint related symbols right now
+    #[serde(rename = "debugger.accent")]
+    pub debugger_accent: Option<String>,
+
+    #[serde(rename = "status_bar.background")]
+    pub status_bar_background: Option<String>,
+
+    #[serde(rename = "title_bar.background")]
+    pub title_bar_background: Option<String>,
+
+    #[serde(rename = "title_bar.inactive_background")]
+    pub title_bar_inactive_background: Option<String>,
+
+    #[serde(rename = "toolbar.background")]
+    pub toolbar_background: Option<String>,
+
+    #[serde(rename = "tab_bar.background")]
+    pub tab_bar_background: Option<String>,
+
+    #[serde(rename = "tab.inactive_background")]
+    pub tab_inactive_background: Option<String>,
+
+    #[serde(rename = "tab.active_background")]
+    pub tab_active_background: Option<String>,
+
+    #[serde(rename = "search.match_background")]
+    pub search_match_background: Option<String>,
+
+    #[serde(rename = "panel.background")]
+    pub panel_background: Option<String>,
+
+    #[serde(rename = "panel.focused_border")]
+    pub panel_focused_border: Option<String>,
+
+    #[serde(rename = "panel.indent_guide")]
+    pub panel_indent_guide: Option<String>,
+
+    #[serde(rename = "panel.indent_guide_hover")]
+    pub panel_indent_guide_hover: Option<String>,
+
+    #[serde(rename = "panel.indent_guide_active")]
+    pub panel_indent_guide_active: Option<String>,
+
+    #[serde(rename = "panel.overlay_background")]
+    pub panel_overlay_background: Option<String>,
+
+    #[serde(rename = "panel.overlay_hover")]
+    pub panel_overlay_hover: Option<String>,
+
+    #[serde(rename = "pane.focused_border")]
+    pub pane_focused_border: Option<String>,
+
+    #[serde(rename = "pane_group.border")]
+    pub pane_group_border: Option<String>,
+
+    /// The deprecated version of `scrollbar.thumb.background`.
+    ///
+    /// Don't use this field.
+    #[serde(rename = "scrollbar_thumb.background", skip_serializing)]
+    #[schemars(skip)]
+    pub deprecated_scrollbar_thumb_background: Option<String>,
+
+    /// The color of the scrollbar thumb.
+    #[serde(rename = "scrollbar.thumb.background")]
+    pub scrollbar_thumb_background: Option<String>,
+
+    /// The color of the scrollbar thumb when hovered over.
+    #[serde(rename = "scrollbar.thumb.hover_background")]
+    pub scrollbar_thumb_hover_background: Option<String>,
+
+    /// The color of the scrollbar thumb whilst being actively dragged.
+    #[serde(rename = "scrollbar.thumb.active_background")]
+    pub scrollbar_thumb_active_background: Option<String>,
+
+    /// The border color of the scrollbar thumb.
+    #[serde(rename = "scrollbar.thumb.border")]
+    pub scrollbar_thumb_border: Option<String>,
+
+    /// The background color of the scrollbar track.
+    #[serde(rename = "scrollbar.track.background")]
+    pub scrollbar_track_background: Option<String>,
+
+    /// The border color of the scrollbar track.
+    #[serde(rename = "scrollbar.track.border")]
+    pub scrollbar_track_border: Option<String>,
+
+    /// The color of the minimap thumb.
+    #[serde(rename = "minimap.thumb.background")]
+    pub minimap_thumb_background: Option<String>,
+
+    /// The color of the minimap thumb when hovered over.
+    #[serde(rename = "minimap.thumb.hover_background")]
+    pub minimap_thumb_hover_background: Option<String>,
+
+    /// The color of the minimap thumb whilst being actively dragged.
+    #[serde(rename = "minimap.thumb.active_background")]
+    pub minimap_thumb_active_background: Option<String>,
+
+    /// The border color of the minimap thumb.
+    #[serde(rename = "minimap.thumb.border")]
+    pub minimap_thumb_border: Option<String>,
+
+    #[serde(rename = "editor.foreground")]
+    pub editor_foreground: Option<String>,
+
+    #[serde(rename = "editor.background")]
+    pub editor_background: Option<String>,
+
+    #[serde(rename = "editor.gutter.background")]
+    pub editor_gutter_background: Option<String>,
+
+    #[serde(rename = "editor.subheader.background")]
+    pub editor_subheader_background: Option<String>,
+
+    #[serde(rename = "editor.active_line.background")]
+    pub editor_active_line_background: Option<String>,
+
+    #[serde(rename = "editor.highlighted_line.background")]
+    pub editor_highlighted_line_background: Option<String>,
+
+    /// Background of active line of debugger
+    #[serde(rename = "editor.debugger_active_line.background")]
+    pub editor_debugger_active_line_background: Option<String>,
+
+    /// Text Color. Used for the text of the line number in the editor gutter.
+    #[serde(rename = "editor.line_number")]
+    pub editor_line_number: Option<String>,
+
+    /// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted.
+    #[serde(rename = "editor.active_line_number")]
+    pub editor_active_line_number: Option<String>,
+
+    /// Text Color. Used for the text of the line number in the editor gutter when the line is hovered over.
+    #[serde(rename = "editor.hover_line_number")]
+    pub editor_hover_line_number: Option<String>,
+
+    /// Text Color. Used to mark invisible characters in the editor.
+    ///
+    /// Example: spaces, tabs, carriage returns, etc.
+    #[serde(rename = "editor.invisible")]
+    pub editor_invisible: Option<String>,
+
+    #[serde(rename = "editor.wrap_guide")]
+    pub editor_wrap_guide: Option<String>,
+
+    #[serde(rename = "editor.active_wrap_guide")]
+    pub editor_active_wrap_guide: Option<String>,
+
+    #[serde(rename = "editor.indent_guide")]
+    pub editor_indent_guide: Option<String>,
+
+    #[serde(rename = "editor.indent_guide_active")]
+    pub editor_indent_guide_active: Option<String>,
+
+    /// Read-access of a symbol, like reading a variable.
+    ///
+    /// A document highlight is a range inside a text document which deserves
+    /// special attention. Usually a document highlight is visualized by changing
+    /// the background color of its range.
+    #[serde(rename = "editor.document_highlight.read_background")]
+    pub editor_document_highlight_read_background: Option<String>,
+
+    /// Read-access of a symbol, like reading a variable.
+    ///
+    /// A document highlight is a range inside a text document which deserves
+    /// special attention. Usually a document highlight is visualized by changing
+    /// the background color of its range.
+    #[serde(rename = "editor.document_highlight.write_background")]
+    pub editor_document_highlight_write_background: Option<String>,
+
+    /// Highlighted brackets background color.
+    ///
+    /// Matching brackets in the cursor scope are highlighted with this background color.
+    #[serde(rename = "editor.document_highlight.bracket_background")]
+    pub editor_document_highlight_bracket_background: Option<String>,
+
+    /// Terminal background color.
+    #[serde(rename = "terminal.background")]
+    pub terminal_background: Option<String>,
+
+    /// Terminal foreground color.
+    #[serde(rename = "terminal.foreground")]
+    pub terminal_foreground: Option<String>,
+
+    /// Terminal ANSI background color.
+    #[serde(rename = "terminal.ansi.background")]
+    pub terminal_ansi_background: Option<String>,
+
+    /// Bright terminal foreground color.
+    #[serde(rename = "terminal.bright_foreground")]
+    pub terminal_bright_foreground: Option<String>,
+
+    /// Dim terminal foreground color.
+    #[serde(rename = "terminal.dim_foreground")]
+    pub terminal_dim_foreground: Option<String>,
+
+    /// Black ANSI terminal color.
+    #[serde(rename = "terminal.ansi.black")]
+    pub terminal_ansi_black: Option<String>,
+
+    /// Bright black ANSI terminal color.
+    #[serde(rename = "terminal.ansi.bright_black")]
+    pub terminal_ansi_bright_black: Option<String>,
+
+    /// Dim black ANSI terminal color.
+    #[serde(rename = "terminal.ansi.dim_black")]
+    pub terminal_ansi_dim_black: Option<String>,
+
+    /// Red ANSI terminal color.
+    #[serde(rename = "terminal.ansi.red")]
+    pub terminal_ansi_red: Option<String>,
+
+    /// Bright red ANSI terminal color.
+    #[serde(rename = "terminal.ansi.bright_red")]
+    pub terminal_ansi_bright_red: Option<String>,
+
+    /// Dim red ANSI terminal color.
+    #[serde(rename = "terminal.ansi.dim_red")]
+    pub terminal_ansi_dim_red: Option<String>,
+
+    /// Green ANSI terminal color.
+    #[serde(rename = "terminal.ansi.green")]
+    pub terminal_ansi_green: Option<String>,
+
+    /// Bright green ANSI terminal color.
+    #[serde(rename = "terminal.ansi.bright_green")]
+    pub terminal_ansi_bright_green: Option<String>,
+
+    /// Dim green ANSI terminal color.
+    #[serde(rename = "terminal.ansi.dim_green")]
+    pub terminal_ansi_dim_green: Option<String>,
+
+    /// Yellow ANSI terminal color.
+    #[serde(rename = "terminal.ansi.yellow")]
+    pub terminal_ansi_yellow: Option<String>,
+
+    /// Bright yellow ANSI terminal color.
+    #[serde(rename = "terminal.ansi.bright_yellow")]
+    pub terminal_ansi_bright_yellow: Option<String>,
+
+    /// Dim yellow ANSI terminal color.
+    #[serde(rename = "terminal.ansi.dim_yellow")]
+    pub terminal_ansi_dim_yellow: Option<String>,
+
+    /// Blue ANSI terminal color.
+    #[serde(rename = "terminal.ansi.blue")]
+    pub terminal_ansi_blue: Option<String>,
+
+    /// Bright blue ANSI terminal color.
+    #[serde(rename = "terminal.ansi.bright_blue")]
+    pub terminal_ansi_bright_blue: Option<String>,
+
+    /// Dim blue ANSI terminal color.
+    #[serde(rename = "terminal.ansi.dim_blue")]
+    pub terminal_ansi_dim_blue: Option<String>,
+
+    /// Magenta ANSI terminal color.
+    #[serde(rename = "terminal.ansi.magenta")]
+    pub terminal_ansi_magenta: Option<String>,
+
+    /// Bright magenta ANSI terminal color.
+    #[serde(rename = "terminal.ansi.bright_magenta")]
+    pub terminal_ansi_bright_magenta: Option<String>,
+
+    /// Dim magenta ANSI terminal color.
+    #[serde(rename = "terminal.ansi.dim_magenta")]
+    pub terminal_ansi_dim_magenta: Option<String>,
+
+    /// Cyan ANSI terminal color.
+    #[serde(rename = "terminal.ansi.cyan")]
+    pub terminal_ansi_cyan: Option<String>,
+
+    /// Bright cyan ANSI terminal color.
+    #[serde(rename = "terminal.ansi.bright_cyan")]
+    pub terminal_ansi_bright_cyan: Option<String>,
+
+    /// Dim cyan ANSI terminal color.
+    #[serde(rename = "terminal.ansi.dim_cyan")]
+    pub terminal_ansi_dim_cyan: Option<String>,
+
+    /// White ANSI terminal color.
+    #[serde(rename = "terminal.ansi.white")]
+    pub terminal_ansi_white: Option<String>,
+
+    /// Bright white ANSI terminal color.
+    #[serde(rename = "terminal.ansi.bright_white")]
+    pub terminal_ansi_bright_white: Option<String>,
+
+    /// Dim white ANSI terminal color.
+    #[serde(rename = "terminal.ansi.dim_white")]
+    pub terminal_ansi_dim_white: Option<String>,
+
+    #[serde(rename = "link_text.hover")]
+    pub link_text_hover: Option<String>,
+
+    /// Added version control color.
+    #[serde(rename = "version_control.added")]
+    pub version_control_added: Option<String>,
+
+    /// Deleted version control color.
+    #[serde(rename = "version_control.deleted")]
+    pub version_control_deleted: Option<String>,
+
+    /// Modified version control color.
+    #[serde(rename = "version_control.modified")]
+    pub version_control_modified: Option<String>,
+
+    /// Renamed version control color.
+    #[serde(rename = "version_control.renamed")]
+    pub version_control_renamed: Option<String>,
+
+    /// Conflict version control color.
+    #[serde(rename = "version_control.conflict")]
+    pub version_control_conflict: Option<String>,
+
+    /// Ignored version control color.
+    #[serde(rename = "version_control.ignored")]
+    pub version_control_ignored: Option<String>,
+
+    /// Background color for row highlights of "ours" regions in merge conflicts.
+    #[serde(rename = "version_control.conflict_marker.ours")]
+    pub version_control_conflict_marker_ours: Option<String>,
+
+    /// Background color for row highlights of "theirs" regions in merge conflicts.
+    #[serde(rename = "version_control.conflict_marker.theirs")]
+    pub version_control_conflict_marker_theirs: Option<String>,
+
+    /// Deprecated in favor of `version_control_conflict_marker_ours`.
+    #[deprecated]
+    pub version_control_conflict_ours_background: Option<String>,
+
+    /// Deprecated in favor of `version_control_conflict_marker_theirs`.
+    #[deprecated]
+    pub version_control_conflict_theirs_background: Option<String>,
+}
+
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[serde(default)]
+pub struct HighlightStyleContent {
+    pub color: Option<String>,
+
+    #[serde(deserialize_with = "treat_error_as_none")]
+    pub background_color: Option<String>,
+
+    #[serde(deserialize_with = "treat_error_as_none")]
+    pub font_style: Option<FontStyleContent>,
+
+    #[serde(deserialize_with = "treat_error_as_none")]
+    pub font_weight: Option<FontWeightContent>,
+}
+
+impl HighlightStyleContent {
+    pub fn is_empty(&self) -> bool {
+        self.color.is_none()
+            && self.background_color.is_none()
+            && self.font_style.is_none()
+            && self.font_weight.is_none()
+    }
+}
+
+fn treat_error_as_none<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
+where
+    T: Deserialize<'de>,
+    D: Deserializer<'de>,
+{
+    let value: Value = Deserialize::deserialize(deserializer)?;
+    Ok(T::deserialize(value).ok())
+}
+
+#[skip_serializing_none]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[serde(default)]
+pub struct StatusColorsContent {
+    /// Indicates some kind of conflict, like a file changed on disk while it was open, or
+    /// merge conflicts in a Git repository.
+    #[serde(rename = "conflict")]
+    pub conflict: Option<String>,
+
+    #[serde(rename = "conflict.background")]
+    pub conflict_background: Option<String>,
+
+    #[serde(rename = "conflict.border")]
+    pub conflict_border: Option<String>,
+
+    /// Indicates something new, like a new file added to a Git repository.
+    #[serde(rename = "created")]
+    pub created: Option<String>,
+
+    #[serde(rename = "created.background")]
+    pub created_background: Option<String>,
+
+    #[serde(rename = "created.border")]
+    pub created_border: Option<String>,
+
+    /// Indicates that something no longer exists, like a deleted file.
+    #[serde(rename = "deleted")]
+    pub deleted: Option<String>,
+
+    #[serde(rename = "deleted.background")]
+    pub deleted_background: Option<String>,
+
+    #[serde(rename = "deleted.border")]
+    pub deleted_border: Option<String>,
+
+    /// Indicates a system error, a failed operation or a diagnostic error.
+    #[serde(rename = "error")]
+    pub error: Option<String>,
+
+    #[serde(rename = "error.background")]
+    pub error_background: Option<String>,
+
+    #[serde(rename = "error.border")]
+    pub error_border: Option<String>,
+
+    /// Represents a hidden status, such as a file being hidden in a file tree.
+    #[serde(rename = "hidden")]
+    pub hidden: Option<String>,
+
+    #[serde(rename = "hidden.background")]
+    pub hidden_background: Option<String>,
+
+    #[serde(rename = "hidden.border")]
+    pub hidden_border: Option<String>,
+
+    /// Indicates a hint or some kind of additional information.
+    #[serde(rename = "hint")]
+    pub hint: Option<String>,
+
+    #[serde(rename = "hint.background")]
+    pub hint_background: Option<String>,
+
+    #[serde(rename = "hint.border")]
+    pub hint_border: Option<String>,
+
+    /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git.
+    #[serde(rename = "ignored")]
+    pub ignored: Option<String>,
+
+    #[serde(rename = "ignored.background")]
+    pub ignored_background: Option<String>,
+
+    #[serde(rename = "ignored.border")]
+    pub ignored_border: Option<String>,
+
+    /// Represents informational status updates or messages.
+    #[serde(rename = "info")]
+    pub info: Option<String>,
+
+    #[serde(rename = "info.background")]
+    pub info_background: Option<String>,
+
+    #[serde(rename = "info.border")]
+    pub info_border: Option<String>,
+
+    /// Indicates a changed or altered status, like a file that has been edited.
+    #[serde(rename = "modified")]
+    pub modified: Option<String>,
+
+    #[serde(rename = "modified.background")]
+    pub modified_background: Option<String>,
+
+    #[serde(rename = "modified.border")]
+    pub modified_border: Option<String>,
+
+    /// Indicates something that is predicted, like automatic code completion, or generated code.
+    #[serde(rename = "predictive")]
+    pub predictive: Option<String>,
+
+    #[serde(rename = "predictive.background")]
+    pub predictive_background: Option<String>,
+
+    #[serde(rename = "predictive.border")]
+    pub predictive_border: Option<String>,
+
+    /// Represents a renamed status, such as a file that has been renamed.
+    #[serde(rename = "renamed")]
+    pub renamed: Option<String>,
+
+    #[serde(rename = "renamed.background")]
+    pub renamed_background: Option<String>,
+
+    #[serde(rename = "renamed.border")]
+    pub renamed_border: Option<String>,
+
+    /// Indicates a successful operation or task completion.
+    #[serde(rename = "success")]
+    pub success: Option<String>,
+
+    #[serde(rename = "success.background")]
+    pub success_background: Option<String>,
+
+    #[serde(rename = "success.border")]
+    pub success_border: Option<String>,
+
+    /// Indicates some kind of unreachable status, like a block of code that can never be reached.
+    #[serde(rename = "unreachable")]
+    pub unreachable: Option<String>,
+
+    #[serde(rename = "unreachable.background")]
+    pub unreachable_background: Option<String>,
+
+    #[serde(rename = "unreachable.border")]
+    pub unreachable_border: Option<String>,
+
+    /// Represents a warning status, like an operation that is about to fail.
+    #[serde(rename = "warning")]
+    pub warning: Option<String>,
+
+    #[serde(rename = "warning.background")]
+    pub warning_background: Option<String>,
+
+    #[serde(rename = "warning.border")]
+    pub warning_border: Option<String>,
+}
+
+/// The background appearance of the window.
+#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum WindowBackgroundContent {
+    Opaque,
+    Transparent,
+    Blurred,
+}
+
+impl Into<gpui::WindowBackgroundAppearance> for WindowBackgroundContent {
+    fn into(self) -> gpui::WindowBackgroundAppearance {
+        match self {
+            WindowBackgroundContent::Opaque => gpui::WindowBackgroundAppearance::Opaque,
+            WindowBackgroundContent::Transparent => gpui::WindowBackgroundAppearance::Transparent,
+            WindowBackgroundContent::Blurred => gpui::WindowBackgroundAppearance::Blurred,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub enum FontStyleContent {
+    Normal,
+    Italic,
+    Oblique,
+}
+
+impl From<FontStyleContent> for FontStyle {
+    fn from(value: FontStyleContent) -> Self {
+        match value {
+            FontStyleContent::Normal => FontStyle::Normal,
+            FontStyleContent::Italic => FontStyle::Italic,
+            FontStyleContent::Oblique => FontStyle::Oblique,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq)]
+#[repr(u16)]
+pub enum FontWeightContent {
+    Thin = 100,
+    ExtraLight = 200,
+    Light = 300,
+    Normal = 400,
+    Medium = 500,
+    Semibold = 600,
+    Bold = 700,
+    ExtraBold = 800,
+    Black = 900,
+}
+
+impl From<FontWeightContent> for FontWeight {
+    fn from(value: FontWeightContent) -> Self {
+        match value {
+            FontWeightContent::Thin => FontWeight::THIN,
+            FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT,
+            FontWeightContent::Light => FontWeight::LIGHT,
+            FontWeightContent::Normal => FontWeight::NORMAL,
+            FontWeightContent::Medium => FontWeight::MEDIUM,
+            FontWeightContent::Semibold => FontWeight::SEMIBOLD,
+            FontWeightContent::Bold => FontWeight::BOLD,
+            FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD,
+            FontWeightContent::Black => FontWeight::BLACK,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde_json::json;
+
+    #[test]
+    fn test_buffer_line_height_deserialize_valid() {
+        assert_eq!(
+            serde_json::from_value::<BufferLineHeight>(json!("comfortable")).unwrap(),
+            BufferLineHeight::Comfortable
+        );
+        assert_eq!(
+            serde_json::from_value::<BufferLineHeight>(json!("standard")).unwrap(),
+            BufferLineHeight::Standard
+        );
+        assert_eq!(
+            serde_json::from_value::<BufferLineHeight>(json!({"custom": 1.0})).unwrap(),
+            BufferLineHeight::Custom(1.0)
+        );
+        assert_eq!(
+            serde_json::from_value::<BufferLineHeight>(json!({"custom": 1.5})).unwrap(),
+            BufferLineHeight::Custom(1.5)
+        );
+    }
+
+    #[test]
+    fn test_buffer_line_height_deserialize_invalid() {
+        assert!(
+            serde_json::from_value::<BufferLineHeight>(json!({"custom": 0.99}))
+                .err()
+                .unwrap()
+                .to_string()
+                .contains("buffer_line_height.custom must be at least 1.0")
+        );
+        assert!(
+            serde_json::from_value::<BufferLineHeight>(json!({"custom": 0.0}))
+                .err()
+                .unwrap()
+                .to_string()
+                .contains("buffer_line_height.custom must be at least 1.0")
+        );
+        assert!(
+            serde_json::from_value::<BufferLineHeight>(json!({"custom": -1.0}))
+                .err()
+                .unwrap()
+                .to_string()
+                .contains("buffer_line_height.custom must be at least 1.0")
+        );
+    }
+}

crates/settings/src/settings_content/workspace.rs πŸ”—

@@ -0,0 +1,440 @@
+use std::num::NonZeroUsize;
+
+use collections::HashMap;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use serde_with::skip_serializing_none;
+
+use crate::{DockPosition, DockSide, ScrollbarSettingsContent, ShowIndentGuides};
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+pub struct WorkspaceSettingsContent {
+    /// Active pane styling settings.
+    pub active_pane_modifiers: Option<ActivePanelModifiers>,
+    /// Layout mode for the bottom dock
+    ///
+    /// Default: contained
+    pub bottom_dock_layout: Option<BottomDockLayout>,
+    /// Direction to split horizontally.
+    ///
+    /// Default: "up"
+    pub pane_split_direction_horizontal: Option<PaneSplitDirectionHorizontal>,
+    /// Direction to split vertically.
+    ///
+    /// Default: "left"
+    pub pane_split_direction_vertical: Option<PaneSplitDirectionVertical>,
+    /// Centered layout related settings.
+    pub centered_layout: Option<CenteredLayoutSettings>,
+    /// Whether or not to prompt the user to confirm before closing the application.
+    ///
+    /// Default: false
+    pub confirm_quit: Option<bool>,
+    /// Whether or not to show the call status icon in the status bar.
+    ///
+    /// Default: true
+    pub show_call_status_icon: Option<bool>,
+    /// When to automatically save edited buffers.
+    ///
+    /// Default: off
+    pub autosave: Option<AutosaveSetting>,
+    /// Controls previous session restoration in freshly launched Zed instance.
+    /// Values: none, last_workspace, last_session
+    /// Default: last_session
+    pub restore_on_startup: Option<RestoreOnStartupBehavior>,
+    /// Whether to attempt to restore previous file's state when opening it again.
+    /// The state is stored per pane.
+    /// When disabled, defaults are applied instead of the state restoration.
+    ///
+    /// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane.
+    /// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default.
+    ///
+    /// Default: true
+    pub restore_on_file_reopen: Option<bool>,
+    /// The size of the workspace split drop targets on the outer edges.
+    /// Given as a fraction that will be multiplied by the smaller dimension of the workspace.
+    ///
+    /// Default: `0.2` (20% of the smaller dimension of the workspace)
+    pub drop_target_size: Option<f32>,
+    /// Whether to close the window when using 'close active item' on a workspace with no tabs
+    ///
+    /// Default: auto ("on" on macOS, "off" otherwise)
+    pub when_closing_with_no_tabs: Option<CloseWindowWhenNoItems>,
+    /// Whether to use the system provided dialogs for Open and Save As.
+    /// When set to false, Zed will use the built-in keyboard-first pickers.
+    ///
+    /// Default: true
+    pub use_system_path_prompts: Option<bool>,
+    /// Whether to use the system provided prompts.
+    /// When set to false, Zed will use the built-in prompts.
+    /// Note that this setting has no effect on Linux, where Zed will always
+    /// use the built-in prompts.
+    ///
+    /// Default: true
+    pub use_system_prompts: Option<bool>,
+    /// Aliases for the command palette. When you type a key in this map,
+    /// it will be assumed to equal the value.
+    ///
+    /// Default: true
+    #[serde(default)]
+    pub command_aliases: HashMap<String, String>,
+    /// Maximum open tabs in a pane. Will not close an unsaved
+    /// tab. Set to `None` for unlimited tabs.
+    ///
+    /// Default: none
+    pub max_tabs: Option<NonZeroUsize>,
+    /// What to do when the last window is closed
+    ///
+    /// Default: auto (nothing on macOS, "app quit" otherwise)
+    pub on_last_window_closed: Option<OnLastWindowClosed>,
+    /// Whether to resize all the panels in a dock when resizing the dock.
+    ///
+    /// Default: ["left"]
+    pub resize_all_panels_in_dock: Option<Vec<DockPosition>>,
+    /// Whether to automatically close files that have been deleted on disk.
+    ///
+    /// Default: false
+    pub close_on_file_delete: Option<bool>,
+    /// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only).
+    ///
+    /// Default: false
+    pub use_system_window_tabs: Option<bool>,
+    /// Whether to show padding for zoomed panels.
+    /// When enabled, zoomed bottom panels will have some top padding,
+    /// while zoomed left/right panels will have padding to the right/left (respectively).
+    ///
+    /// Default: true
+    pub zoomed_padding: Option<bool>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct ItemSettingsContent {
+    /// Whether to show the Git file status on a tab item.
+    ///
+    /// Default: false
+    pub git_status: Option<bool>,
+    /// Position of the close button in a tab.
+    ///
+    /// Default: right
+    pub close_position: Option<ClosePosition>,
+    /// Whether to show the file icon for a tab.
+    ///
+    /// Default: false
+    pub file_icons: Option<bool>,
+    /// What to do after closing the current tab.
+    ///
+    /// Default: history
+    pub activate_on_close: Option<ActivateOnClose>,
+    /// Which files containing diagnostic errors/warnings to mark in the tabs.
+    /// This setting can take the following three values:
+    ///
+    /// Default: off
+    pub show_diagnostics: Option<ShowDiagnostics>,
+    /// Whether to always show the close button on tabs.
+    ///
+    /// Default: false
+    pub show_close_button: Option<ShowCloseButton>,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct PreviewTabsSettingsContent {
+    /// Whether to show opened editors as preview tabs.
+    /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic.
+    ///
+    /// Default: true
+    pub enabled: Option<bool>,
+    /// Whether to open tabs in preview mode when selected from the file finder.
+    ///
+    /// Default: false
+    pub enable_preview_from_file_finder: Option<bool>,
+    /// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
+    ///
+    /// Default: false
+    pub enable_preview_from_code_navigation: Option<bool>,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum ClosePosition {
+    Left,
+    #[default]
+    Right,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "lowercase")]
+pub enum ShowCloseButton {
+    Always,
+    #[default]
+    Hover,
+    Hidden,
+}
+
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowDiagnostics {
+    #[default]
+    Off,
+    Errors,
+    All,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ActivateOnClose {
+    #[default]
+    History,
+    Neighbour,
+    LeftNeighbour,
+}
+
+#[skip_serializing_none]
+#[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct ActivePanelModifiers {
+    /// Size of the border surrounding the active pane.
+    /// When set to 0, the active pane doesn't have any border.
+    /// The border is drawn inset.
+    ///
+    /// Default: `0.0`
+    pub border_size: Option<f32>,
+    /// Opacity of inactive panels.
+    /// When set to 1.0, the inactive panes have the same opacity as the active one.
+    /// If set to 0, the inactive panes content will not be visible at all.
+    /// Values are clamped to the [0.0, 1.0] range.
+    ///
+    /// Default: `1.0`
+    pub inactive_opacity: Option<f32>,
+}
+
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum BottomDockLayout {
+    /// Contained between the left and right docks
+    #[default]
+    Contained,
+    /// Takes up the full width of the window
+    Full,
+    /// Extends under the left dock while snapping to the right dock
+    LeftAligned,
+    /// Extends under the right dock while snapping to the left dock
+    RightAligned,
+}
+
+#[derive(Copy, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum CloseWindowWhenNoItems {
+    /// Match platform conventions by default, so "on" on macOS and "off" everywhere else
+    #[default]
+    PlatformDefault,
+    /// Close the window when there are no tabs
+    CloseWindow,
+    /// Leave the window open when there are no tabs
+    KeepWindowOpen,
+}
+
+impl CloseWindowWhenNoItems {
+    pub fn should_close(&self) -> bool {
+        match self {
+            CloseWindowWhenNoItems::PlatformDefault => cfg!(target_os = "macos"),
+            CloseWindowWhenNoItems::CloseWindow => true,
+            CloseWindowWhenNoItems::KeepWindowOpen => false,
+        }
+    }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum RestoreOnStartupBehavior {
+    /// Always start with an empty editor
+    None,
+    /// Restore the workspace that was closed last.
+    LastWorkspace,
+    /// Restore all workspaces that were open when quitting Zed.
+    #[default]
+    LastSession,
+}
+
+#[skip_serializing_none]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
+pub struct TabBarSettingsContent {
+    /// Whether or not to show the tab bar in the editor.
+    ///
+    /// Default: true
+    pub show: Option<bool>,
+    /// Whether or not to show the navigation history buttons in the tab bar.
+    ///
+    /// Default: true
+    pub show_nav_history_buttons: Option<bool>,
+    /// Whether or not to show the tab bar buttons.
+    ///
+    /// Default: true
+    pub show_tab_bar_buttons: Option<bool>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum AutosaveSetting {
+    /// Disable autosave.
+    Off,
+    /// Save after inactivity period of `milliseconds`.
+    AfterDelay { milliseconds: u64 },
+    /// Autosave when focus changes.
+    OnFocusChange,
+    /// Autosave when the active window changes.
+    OnWindowChange,
+}
+
+impl AutosaveSetting {
+    pub fn should_save_on_close(&self) -> bool {
+        matches!(
+            &self,
+            AutosaveSetting::OnFocusChange
+                | AutosaveSetting::OnWindowChange
+                | AutosaveSetting::AfterDelay { .. }
+        )
+    }
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum PaneSplitDirectionHorizontal {
+    Up,
+    Down,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum PaneSplitDirectionVertical {
+    Left,
+    Right,
+}
+
+#[skip_serializing_none]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[serde(rename_all = "snake_case")]
+pub struct CenteredLayoutSettings {
+    /// The relative width of the left padding of the central pane from the
+    /// workspace when the centered layout is used.
+    ///
+    /// Default: 0.2
+    pub left_padding: Option<f32>,
+    // The relative width of the right padding of the central pane from the
+    // workspace when the centered layout is used.
+    ///
+    /// Default: 0.2
+    pub right_padding: Option<f32>,
+}
+
+#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum OnLastWindowClosed {
+    /// Match platform conventions by default, so don't quit on macOS, and quit on other platforms
+    #[default]
+    PlatformDefault,
+    /// Quit the application the last window is closed
+    QuitApp,
+}
+
+impl OnLastWindowClosed {
+    pub fn is_quit_app(&self) -> bool {
+        match self {
+            OnLastWindowClosed::PlatformDefault => false,
+            OnLastWindowClosed::QuitApp => true,
+        }
+    }
+}
+
+#[skip_serializing_none]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct ProjectPanelSettingsContent {
+    /// Whether to show the project panel button in the status bar.
+    ///
+    /// Default: true
+    pub button: Option<bool>,
+    /// Whether to hide gitignore files in the project panel.
+    ///
+    /// Default: false
+    pub hide_gitignore: Option<bool>,
+    /// Customize default width (in pixels) taken by project panel
+    ///
+    /// Default: 240
+    pub default_width: Option<f32>,
+    /// The position of project panel
+    ///
+    /// Default: left
+    pub dock: Option<DockSide>,
+    /// Spacing between worktree entries in the project panel.
+    ///
+    /// Default: comfortable
+    pub entry_spacing: Option<ProjectPanelEntrySpacing>,
+    /// Whether to show file icons in the project panel.
+    ///
+    /// Default: true
+    pub file_icons: Option<bool>,
+    /// Whether to show folder icons or chevrons for directories in the project panel.
+    ///
+    /// Default: true
+    pub folder_icons: Option<bool>,
+    /// Whether to show the git status in the project panel.
+    ///
+    /// Default: true
+    pub git_status: Option<bool>,
+    /// Amount of indentation (in pixels) for nested items.
+    ///
+    /// Default: 20
+    pub indent_size: Option<f32>,
+    /// Whether to reveal it in the project panel automatically,
+    /// when a corresponding project entry becomes active.
+    /// Gitignored entries are never auto revealed.
+    ///
+    /// Default: true
+    pub auto_reveal_entries: Option<bool>,
+    /// Whether to fold directories automatically
+    /// when directory has only one directory inside.
+    ///
+    /// Default: true
+    pub auto_fold_dirs: Option<bool>,
+    /// Whether the project panel should open on startup.
+    ///
+    /// Default: true
+    pub starts_open: Option<bool>,
+    /// Scrollbar-related settings
+    pub scrollbar: Option<ScrollbarSettingsContent>,
+    /// Which files containing diagnostic errors/warnings to mark in the project panel.
+    ///
+    /// Default: all
+    pub show_diagnostics: Option<ShowDiagnostics>,
+    /// Settings related to indent guides in the project panel.
+    pub indent_guides: Option<ProjectPanelIndentGuidesSettings>,
+    /// Whether to hide the root entry when only one folder is open in the window.
+    ///
+    /// Default: false
+    pub hide_root: Option<bool>,
+    /// Whether to stick parent directories at top of the project panel.
+    ///
+    /// Default: true
+    pub sticky_scroll: Option<bool>,
+    /// Whether to enable drag-and-drop operations in the project panel.
+    ///
+    /// Default: true
+    pub drag_and_drop: Option<bool>,
+}
+
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ProjectPanelEntrySpacing {
+    /// Comfortable spacing of entries.
+    #[default]
+    Comfortable,
+    /// The standard spacing of entries.
+    Standard,
+}
+
+#[skip_serializing_none]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ProjectPanelIndentGuidesSettings {
+    pub show: Option<ShowIndentGuides>,
+}

crates/settings/src/settings_file.rs πŸ”—

@@ -1,4 +1,4 @@
-use crate::{Settings, settings_store::SettingsStore};
+use crate::{settings_content::SettingsContent, settings_store::SettingsStore};
 use collections::HashSet;
 use fs::{Fs, PathEventKind};
 use futures::{StreamExt, channel::mpsc};
@@ -126,10 +126,10 @@ pub fn watch_config_dir(
     rx
 }
 
-pub fn update_settings_file<T: Settings>(
+pub fn update_settings_file(
     fs: Arc<dyn Fs>,
     cx: &App,
-    update: impl 'static + Send + FnOnce(&mut T::FileContent, &App),
+    update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
 ) {
-    SettingsStore::global(cx).update_settings_file::<T>(fs, update);
+    SettingsStore::global(cx).update_settings_file(fs, update);
 }

crates/settings/src/settings_json.rs πŸ”—

@@ -26,7 +26,6 @@ pub fn update_value_in_json_text<'a>(
     tab_size: usize,
     old_value: &'a Value,
     new_value: &'a Value,
-    preserved_keys: &[&str],
     edits: &mut Vec<(Range<usize>, String)>,
 ) {
     // If the old and new values are both objects, then compare them key by key,
@@ -43,7 +42,6 @@ pub fn update_value_in_json_text<'a>(
                     tab_size,
                     old_sub_value,
                     new_sub_value,
-                    preserved_keys,
                     edits,
                 );
             } else {
@@ -64,17 +62,12 @@ pub fn update_value_in_json_text<'a>(
                     tab_size,
                     &Value::Null,
                     new_sub_value,
-                    preserved_keys,
                     edits,
                 );
             }
             key_path.pop();
         }
-    } else if key_path
-        .last()
-        .is_some_and(|key| preserved_keys.contains(key))
-        || old_value != new_value
-    {
+    } else if old_value != new_value {
         let mut new_value = new_value.clone();
         if let Some(new_object) = new_value.as_object_mut() {
             new_object.retain(|_, v| !v.is_null());

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

@@ -11,29 +11,28 @@ use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGl
 
 use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
 use schemars::JsonSchema;
-use serde::{Serialize, de::DeserializeOwned};
-use serde_json::{Value, json};
+use serde_json::Value;
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId, type_name},
-    env,
     fmt::Debug,
     ops::Range,
     path::{Path, PathBuf},
     str::{self, FromStr},
     sync::Arc,
 };
-use util::{
-    ResultExt as _, merge_non_null_json_value_into,
-    schemars::{DefaultDenyUnknownFields, add_new_subschema},
-};
+use util::{ResultExt as _, schemars::DefaultDenyUnknownFields};
 
 pub type EditorconfigProperties = ec4rs::Properties;
 
 use crate::{
     ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry,
     VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text,
-    settings_ui_core::SettingsUi, update_value_in_json_text,
+    settings_content::{
+        ExtensionsSettingsContent, ProjectSettingsContent, ServerSettingsContent, SettingsContent,
+        UserSettingsContent,
+    },
+    update_value_in_json_text,
 };
 
 pub trait SettingsKey: 'static + Send + Sync {
@@ -48,7 +47,7 @@ pub trait SettingsKey: 'static + Send + Sync {
 /// A value that can be defined as a user setting.
 ///
 /// Settings can be loaded from a combination of multiple JSON files.
-pub trait Settings: 'static + Send + Sync {
+pub trait Settings: 'static + Send + Sync + Sized {
     /// The name of the keys in the [`FileContent`](Self::FileContent) that should
     /// always be written to a settings file, even if their value matches the default
     /// value.
@@ -58,30 +57,16 @@ pub trait Settings: 'static + Send + Sync {
     /// user settings match the current version of the settings.
     const PRESERVED_KEYS: Option<&'static [&'static str]> = None;
 
-    /// The type that is stored in an individual JSON file.
-    type FileContent: Clone
-        + Default
-        + Serialize
-        + DeserializeOwned
-        + JsonSchema
-        + SettingsUi
-        + SettingsKey;
-
-    /*
-     *  let path = Settings
-     *
-     *
-     */
-    /// The logic for combining together values from one or more JSON files into the
-    /// final value for this setting.
+    /// Read the value from default.json.
+    /// This function *should* panic if default values are missing,
+    /// and you should add a default to default.json for documentation.
+    fn from_defaults(content: &SettingsContent, cx: &mut App) -> Self;
+
+    /// Update the value based on the content from the current file.
     ///
-    /// # Warning
-    /// `Self::FileContent` deserialized field names should match with `Self` deserialized field names
-    /// otherwise the field won't be deserialized properly and you will get the error:
-    /// "A default setting must be added to the `default.json` file"
-    fn load(sources: SettingsSources<Self::FileContent>, cx: &mut App) -> Result<Self>
-    where
-        Self: Sized;
+    /// This function *should not* panic if there are problems, as the
+    /// content of user-provided settings files may be incomplete or invalid.
+    fn refine(&mut self, content: &SettingsContent, cx: &mut App);
 
     fn missing_default() -> anyhow::Error {
         anyhow::anyhow!("missing default for: {}", std::any::type_name::<Self>())
@@ -89,7 +74,7 @@ pub trait Settings: 'static + Send + Sync {
 
     /// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known
     /// equivalent settings from a vscode config to our config
-    fn import_from_vscode(vscode: &VsCodeSettings, current: &mut Self::FileContent);
+    fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut SettingsContent) {}
 
     #[track_caller]
     fn register(cx: &mut App)
@@ -146,69 +131,6 @@ pub trait Settings: 'static + Send + Sync {
     }
 }
 
-#[derive(Clone, Copy, Debug)]
-pub struct SettingsSources<'a, T> {
-    /// The default Zed settings.
-    pub default: &'a T,
-    /// Global settings (loaded before user settings).
-    pub global: Option<&'a T>,
-    /// Settings provided by extensions.
-    pub extensions: Option<&'a T>,
-    /// The user settings.
-    pub user: Option<&'a T>,
-    /// The user settings for the current release channel.
-    pub release_channel: Option<&'a T>,
-    /// The user settings for the current operating system.
-    pub operating_system: Option<&'a T>,
-    /// The settings associated with an enabled settings profile
-    pub profile: Option<&'a T>,
-    /// The server's settings.
-    pub server: Option<&'a T>,
-    /// The project settings, ordered from least specific to most specific.
-    pub project: &'a [&'a T],
-}
-
-impl<'a, T: Serialize> SettingsSources<'a, T> {
-    /// Returns an iterator over the default settings as well as all settings customizations.
-    pub fn defaults_and_customizations(&self) -> impl Iterator<Item = &T> {
-        [self.default].into_iter().chain(self.customizations())
-    }
-
-    /// Returns an iterator over all of the settings customizations.
-    pub fn customizations(&self) -> impl Iterator<Item = &T> {
-        self.global
-            .into_iter()
-            .chain(self.extensions)
-            .chain(self.user)
-            .chain(self.release_channel)
-            .chain(self.operating_system)
-            .chain(self.profile)
-            .chain(self.server)
-            .chain(self.project.iter().copied())
-    }
-
-    /// Returns the settings after performing a JSON merge of the provided customizations.
-    ///
-    /// Customizations later in the iterator win out over the earlier ones.
-    pub fn json_merge_with<O: DeserializeOwned>(
-        customizations: impl Iterator<Item = &'a T>,
-    ) -> Result<O> {
-        let mut merged = Value::Null;
-        for value in customizations {
-            merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
-        }
-        Ok(serde_json::from_value(merged)?)
-    }
-
-    /// Returns the settings after performing a JSON merge of the customizations into the
-    /// default settings.
-    ///
-    /// More-specific customizations win out over the less-specific ones.
-    pub fn json_merge<O: DeserializeOwned>(&'a self) -> Result<O> {
-        Self::json_merge_with(self.defaults_and_customizations())
-    }
-}
-
 #[derive(Clone, Copy, Debug)]
 pub struct SettingsLocation<'a> {
     pub worktree_id: WorktreeId,
@@ -218,17 +140,15 @@ pub struct SettingsLocation<'a> {
 /// A set of strongly-typed setting values defined via multiple config files.
 pub struct SettingsStore {
     setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
-    raw_default_settings: Value,
-    raw_global_settings: Option<Value>,
-    raw_user_settings: Value,
-    raw_server_settings: Option<Value>,
-    raw_extension_settings: Value,
-    raw_local_settings: BTreeMap<(WorktreeId, Arc<Path>), Value>,
+    default_settings: Box<SettingsContent>,
+    user_settings: Option<UserSettingsContent>,
+    global_settings: Option<Box<SettingsContent>>,
+
+    extension_settings: Option<Box<SettingsContent>>,
+    server_settings: Option<Box<SettingsContent>>,
+    local_settings: BTreeMap<(WorktreeId, Arc<Path>), SettingsContent>,
     raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<Path>), (String, Option<Editorconfig>)>,
-    tab_size_callback: Option<(
-        TypeId,
-        Box<dyn Fn(&dyn Any) -> Option<usize> + Send + Sync + 'static>,
-    )>,
+
     _setting_file_updates: Task<()>,
     setting_file_updates_tx:
         mpsc::UnboundedSender<Box<dyn FnOnce(AsyncApp) -> LocalBoxFuture<'static, Result<()>>>>,
@@ -271,51 +191,35 @@ struct SettingValue<T> {
 }
 
 trait AnySettingValue: 'static + Send + Sync {
-    fn key(&self) -> Option<&'static str>;
     fn setting_type_name(&self) -> &'static str;
-    fn deserialize_setting(&self, json: &Value) -> Result<DeserializedSetting> {
-        self.deserialize_setting_with_key(json).1
-    }
-    fn deserialize_setting_with_key(
-        &self,
-        json: &Value,
-    ) -> (Option<&'static str>, Result<DeserializedSetting>);
-    fn load_setting(
-        &self,
-        sources: SettingsSources<DeserializedSetting>,
-        cx: &mut App,
-    ) -> Result<Box<dyn Any>>;
+
+    fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Box<dyn Any>;
+    fn refine(&self, value: &mut dyn Any, s: &[&SettingsContent], cx: &mut App);
+
     fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
     fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)>;
     fn set_global_value(&mut self, value: Box<dyn Any>);
     fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<Path>, value: Box<dyn Any>);
-    fn json_schema(&self, generator: &mut schemars::SchemaGenerator) -> schemars::Schema;
-    fn edits_for_update(
+    fn import_from_vscode(
         &self,
-        raw_settings: &serde_json::Value,
-        tab_size: usize,
         vscode_settings: &VsCodeSettings,
-        text: &mut String,
-        edits: &mut Vec<(Range<usize>, String)>,
+        settings_content: &mut SettingsContent,
     );
-    fn settings_ui_item(&self) -> SettingsUiEntry;
 }
 
-struct DeserializedSetting(Box<dyn Any>);
-
 impl SettingsStore {
-    pub fn new(cx: &App) -> Self {
+    pub fn new(cx: &App, default_settings: &str) -> Self {
         let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded();
+        let default_settings = parse_json_with_comments(default_settings).unwrap();
         Self {
             setting_values: Default::default(),
-            raw_default_settings: json!({}),
-            raw_global_settings: None,
-            raw_user_settings: json!({}),
-            raw_server_settings: None,
-            raw_extension_settings: json!({}),
-            raw_local_settings: Default::default(),
+            default_settings,
+            global_settings: None,
+            server_settings: None,
+            user_settings: None,
+            extension_settings: None,
+            local_settings: BTreeMap::default(),
             raw_editorconfig_settings: BTreeMap::default(),
-            tab_size_callback: Default::default(),
             setting_file_updates_tx,
             _setting_file_updates: cx.spawn(async move |cx| {
                 while let Some(setting_file_update) = setting_file_updates_rx.next().await {
@@ -354,71 +258,38 @@ impl SettingsStore {
             local_values: Vec::new(),
         }));
 
-        if let Some(default_settings) = setting_value
-            .deserialize_setting(&self.raw_default_settings)
-            .log_err()
-        {
-            let user_value = setting_value
-                .deserialize_setting(&self.raw_user_settings)
-                .log_err();
-
-            let mut release_channel_value = None;
-            if let Some(release_settings) = &self
-                .raw_user_settings
-                .get(release_channel::RELEASE_CHANNEL.dev_name())
-            {
-                release_channel_value = setting_value
-                    .deserialize_setting(release_settings)
-                    .log_err();
-            }
-
-            let mut os_settings_value = None;
-            if let Some(os_settings) = &self.raw_user_settings.get(env::consts::OS) {
-                os_settings_value = setting_value.deserialize_setting(os_settings).log_err();
-            }
+        let mut refinements = Vec::default();
 
-            let mut profile_value = None;
-            if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>()
-                && let Some(profiles) = self.raw_user_settings.get("profiles")
-                && let Some(profile_settings) = profiles.get(&active_profile.0)
-            {
-                profile_value = setting_value
-                    .deserialize_setting(profile_settings)
-                    .log_err();
-            }
+        if let Some(extension_settings) = self.extension_settings.as_deref() {
+            refinements.push(extension_settings)
+        }
 
-            let server_value = self
-                .raw_server_settings
-                .as_ref()
-                .and_then(|server_setting| {
-                    setting_value.deserialize_setting(server_setting).log_err()
-                });
+        if let Some(global_settings) = self.global_settings.as_deref() {
+            refinements.push(global_settings)
+        }
 
-            let extension_value = setting_value
-                .deserialize_setting(&self.raw_extension_settings)
-                .log_err();
-
-            if let Some(setting) = setting_value
-                .load_setting(
-                    SettingsSources {
-                        default: &default_settings,
-                        global: None,
-                        extensions: extension_value.as_ref(),
-                        user: user_value.as_ref(),
-                        release_channel: release_channel_value.as_ref(),
-                        operating_system: os_settings_value.as_ref(),
-                        profile: profile_value.as_ref(),
-                        server: server_value.as_ref(),
-                        project: &[],
-                    },
-                    cx,
-                )
-                .context("A default setting must be added to the `default.json` file")
-                .log_err()
-            {
-                setting_value.set_global_value(setting);
+        if let Some(user_settings) = self.user_settings.as_ref() {
+            refinements.push(&user_settings.content);
+            if let Some(release_channel) = user_settings.for_release_channel() {
+                refinements.push(release_channel)
+            }
+            if let Some(os) = user_settings.for_os() {
+                refinements.push(os)
             }
+            if let Some(profile) = user_settings.for_profile(cx) {
+                refinements.push(profile)
+            }
+        }
+
+        if let Some(server_settings) = self.server_settings.as_ref() {
+            refinements.push(server_settings)
         }
+        let mut value = T::from_defaults(&self.default_settings, cx);
+        for refinement in refinements {
+            value.refine(refinement, cx)
+        }
+
+        setting_value.set_global_value(Box::new(value));
     }
 
     /// Get the value of a setting.
@@ -476,13 +347,17 @@ impl SettingsStore {
     ///
     /// For user-facing functionality use the typed setting interface.
     /// (e.g. ProjectSettings::get_global(cx))
-    pub fn raw_user_settings(&self) -> &Value {
-        &self.raw_user_settings
+    pub fn raw_user_settings(&self) -> Option<&UserSettingsContent> {
+        self.user_settings.as_ref()
     }
 
     /// Replaces current settings with the values from the given JSON.
-    pub fn set_raw_user_settings(&mut self, new_settings: Value, cx: &mut App) -> Result<()> {
-        self.raw_user_settings = new_settings;
+    pub fn set_raw_user_settings(
+        &mut self,
+        new_settings: UserSettingsContent,
+        cx: &mut App,
+    ) -> Result<()> {
+        self.user_settings = Some(new_settings);
         self.recompute_values(None, cx)?;
         Ok(())
     }
@@ -493,38 +368,31 @@ impl SettingsStore {
         new_settings: Option<Value>,
         cx: &mut App,
     ) -> Result<()> {
-        self.raw_server_settings = new_settings;
+        // Rewrite the server settings into a content type
+        self.server_settings = new_settings
+            .map(|settings| settings.to_string())
+            .and_then(|str| parse_json_with_comments::<SettingsContent>(&str).ok())
+            .map(Box::new);
+
         self.recompute_values(None, cx)?;
         Ok(())
     }
 
     /// Get the configured settings profile names.
     pub fn configured_settings_profiles(&self) -> impl Iterator<Item = &str> {
-        self.raw_user_settings
-            .get("profiles")
-            .and_then(|v| v.as_object())
-            .into_iter()
-            .flat_map(|obj| obj.keys())
-            .map(|s| s.as_str())
-    }
-
-    /// Access the raw JSON value of the global settings.
-    pub fn raw_global_settings(&self) -> Option<&Value> {
-        self.raw_global_settings.as_ref()
+        self.user_settings
+            .iter()
+            .flat_map(|settings| settings.profiles.keys().map(|k| k.as_str()))
     }
 
     /// Access the raw JSON value of the default settings.
-    pub fn raw_default_settings(&self) -> &Value {
-        &self.raw_default_settings
+    pub fn raw_default_settings(&self) -> &SettingsContent {
+        &self.default_settings
     }
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut App) -> Self {
-        let mut this = Self::new(cx);
-        this.set_default_settings(&crate::test_settings(), cx)
-            .unwrap();
-        this.set_user_settings("{}", cx).unwrap();
-        this
+        Self::new(cx, &crate::test_settings())
     }
 
     /// Updates the value of a setting in the user's global configuration.
@@ -532,13 +400,18 @@ impl SettingsStore {
     /// This is only for tests. Normally, settings are only loaded from
     /// JSON files.
     #[cfg(any(test, feature = "test-support"))]
-    pub fn update_user_settings<T: Settings>(
+    pub fn update_user_settings(
         &mut self,
         cx: &mut App,
-        update: impl FnOnce(&mut T::FileContent),
+        update: impl FnOnce(&mut SettingsContent),
     ) {
-        let old_text = serde_json::to_string(&self.raw_user_settings).unwrap();
-        let new_text = self.new_text_for_update::<T>(old_text, update);
+        let mut content = self.user_settings.clone().unwrap_or_default().content;
+        update(&mut content);
+        let new_text = serde_json::to_string(&UserSettingsContent {
+            content,
+            ..Default::default()
+        })
+        .unwrap();
         self.set_user_settings(&new_text, cx).unwrap();
     }
 
@@ -637,14 +510,14 @@ impl SettingsStore {
         self.update_settings_file_inner(fs, update)
     }
 
-    pub fn update_settings_file<T: Settings>(
+    pub fn update_settings_file(
         &self,
         fs: Arc<dyn Fs>,
-        update: impl 'static + Send + FnOnce(&mut T::FileContent, &App),
+        update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
     ) {
         _ = self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| {
             cx.read_global(|store: &SettingsStore, cx| {
-                store.new_text_for_update::<T>(old_text, |content| update(content, cx))
+                store.new_text_for_update(old_text, |content| update(content, cx))
             })
         });
     }
@@ -662,21 +535,19 @@ impl SettingsStore {
     }
 
     pub fn settings_ui_items(&self) -> impl IntoIterator<Item = SettingsUiEntry> {
-        self.setting_values
-            .values()
-            .map(|item| item.settings_ui_item())
+        [].into_iter()
     }
 }
 
 impl SettingsStore {
     /// Updates the value of a setting in a JSON file, returning the new text
     /// for that JSON file.
-    pub fn new_text_for_update<T: Settings>(
+    pub fn new_text_for_update(
         &self,
         old_text: String,
-        update: impl FnOnce(&mut T::FileContent),
+        update: impl FnOnce(&mut SettingsContent),
     ) -> String {
-        let edits = self.edits_for_update::<T>(&old_text, update);
+        let edits = self.edits_for_update(&old_text, update);
         let mut new_text = old_text;
         for (range, replacement) in edits.into_iter() {
             new_text.replace_range(range, &replacement);
@@ -684,52 +555,30 @@ impl SettingsStore {
         new_text
     }
 
-    pub fn get_vscode_edits(&self, mut old_text: String, vscode: &VsCodeSettings) -> String {
-        let mut new_text = old_text.clone();
-        let mut edits: Vec<(Range<usize>, String)> = Vec::new();
-        let raw_settings = parse_json_with_comments::<Value>(&old_text).unwrap_or_default();
-        let tab_size = self.json_tab_size();
-        for v in self.setting_values.values() {
-            v.edits_for_update(&raw_settings, tab_size, vscode, &mut old_text, &mut edits);
-        }
-        for (range, replacement) in edits.into_iter() {
-            new_text.replace_range(range, &replacement);
-        }
-        new_text
+    pub fn get_vscode_edits(&self, old_text: String, vscode: &VsCodeSettings) -> String {
+        self.new_text_for_update(old_text, |settings_content| {
+            for v in self.setting_values.values() {
+                v.import_from_vscode(vscode, settings_content)
+            }
+        })
     }
 
     /// Updates the value of a setting in a JSON file, returning a list
     /// of edits to apply to the JSON file.
-    pub fn edits_for_update<T: Settings>(
+    pub fn edits_for_update(
         &self,
         text: &str,
-        update: impl FnOnce(&mut T::FileContent),
+        update: impl FnOnce(&mut SettingsContent),
     ) -> Vec<(Range<usize>, String)> {
-        let setting_type_id = TypeId::of::<T>();
-
-        let preserved_keys = T::PRESERVED_KEYS.unwrap_or_default();
-
-        let setting = self
-            .setting_values
-            .get(&setting_type_id)
-            .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()));
-        let raw_settings = parse_json_with_comments::<Value>(text).unwrap_or_default();
-        let (key, deserialized_setting) = setting.deserialize_setting_with_key(&raw_settings);
-        let old_content = match deserialized_setting {
-            Ok(content) => content.0.downcast::<T::FileContent>().unwrap(),
-            Err(_) => Box::<<T as Settings>::FileContent>::default(),
-        };
+        let old_content: UserSettingsContent =
+            parse_json_with_comments(text).log_err().unwrap_or_default();
         let mut new_content = old_content.clone();
-        update(&mut new_content);
+        update(&mut new_content.content);
 
         let old_value = serde_json::to_value(&old_content).unwrap();
         let new_value = serde_json::to_value(new_content).unwrap();
 
         let mut key_path = Vec::new();
-        if let Some(key) = key {
-            key_path.push(key);
-        }
-
         let mut edits = Vec::new();
         let tab_size = self.json_tab_size();
         let mut text = text.to_string();
@@ -739,35 +588,13 @@ impl SettingsStore {
             tab_size,
             &old_value,
             &new_value,
-            preserved_keys,
             &mut edits,
         );
         edits
     }
 
-    /// Configure the tab sized when updating JSON files.
-    pub fn set_json_tab_size_callback<T: Settings>(
-        &mut self,
-        get_tab_size: fn(&T) -> Option<usize>,
-    ) {
-        self.tab_size_callback = Some((
-            TypeId::of::<T>(),
-            Box::new(move |value| get_tab_size(value.downcast_ref::<T>().unwrap())),
-        ));
-    }
-
     pub fn json_tab_size(&self) -> usize {
-        const DEFAULT_JSON_TAB_SIZE: usize = 2;
-
-        if let Some((setting_type_id, callback)) = &self.tab_size_callback {
-            let setting_value = self.setting_values.get(setting_type_id).unwrap();
-            let value = setting_value.value_for_path(None);
-            if let Some(value) = callback(value) {
-                return value;
-            }
-        }
-
-        DEFAULT_JSON_TAB_SIZE
+        2
     }
 
     /// Sets the default settings via a JSON string.
@@ -778,29 +605,22 @@ impl SettingsStore {
         default_settings_content: &str,
         cx: &mut App,
     ) -> Result<()> {
-        let settings: Value = parse_json_with_comments(default_settings_content)?;
-        anyhow::ensure!(settings.is_object(), "settings must be an object");
-        self.raw_default_settings = settings;
+        self.default_settings = parse_json_with_comments(default_settings_content)?;
         self.recompute_values(None, cx)?;
         Ok(())
     }
 
     /// Sets the user settings via a JSON string.
-    pub fn set_user_settings(
-        &mut self,
-        user_settings_content: &str,
-        cx: &mut App,
-    ) -> Result<Value> {
-        let settings: Value = if user_settings_content.is_empty() {
+    pub fn set_user_settings(&mut self, user_settings_content: &str, cx: &mut App) -> Result<()> {
+        let settings: UserSettingsContent = if user_settings_content.is_empty() {
             parse_json_with_comments("{}")?
         } else {
             parse_json_with_comments(user_settings_content)?
         };
 
-        anyhow::ensure!(settings.is_object(), "settings must be an object");
-        self.raw_user_settings = settings.clone();
+        self.user_settings = Some(settings);
         self.recompute_values(None, cx)?;
-        Ok(settings)
+        Ok(())
     }
 
     /// Sets the global settings via a JSON string.
@@ -808,17 +628,16 @@ impl SettingsStore {
         &mut self,
         global_settings_content: &str,
         cx: &mut App,
-    ) -> Result<Value> {
-        let settings: Value = if global_settings_content.is_empty() {
+    ) -> Result<()> {
+        let settings: SettingsContent = if global_settings_content.is_empty() {
             parse_json_with_comments("{}")?
         } else {
             parse_json_with_comments(global_settings_content)?
         };
 
-        anyhow::ensure!(settings.is_object(), "settings must be an object");
-        self.raw_global_settings = Some(settings.clone());
+        self.global_settings = Some(Box::new(settings));
         self.recompute_values(None, cx)?;
-        Ok(settings)
+        Ok(())
     }
 
     pub fn set_server_settings(
@@ -826,20 +645,20 @@ impl SettingsStore {
         server_settings_content: &str,
         cx: &mut App,
     ) -> Result<()> {
-        let settings: Option<Value> = if server_settings_content.is_empty() {
+        let settings: Option<ServerSettingsContent> = if server_settings_content.is_empty() {
             None
         } else {
             parse_json_with_comments(server_settings_content)?
         };
 
-        anyhow::ensure!(
-            settings
-                .as_ref()
-                .map(|value| value.is_object())
-                .unwrap_or(true),
-            "settings must be an object"
-        );
-        self.raw_server_settings = settings;
+        // Rewrite the server settings into a content type
+        self.server_settings = settings.map(|settings| {
+            Box::new(SettingsContent {
+                project: settings.project,
+                ..Default::default()
+            })
+        });
+
         self.recompute_values(None, cx)?;
         Ok(())
     }
@@ -875,7 +694,7 @@ impl SettingsStore {
             }
             (LocalSettingsKind::Settings, None) => {
                 zed_settings_changed = self
-                    .raw_local_settings
+                    .local_settings
                     .remove(&(root_id, directory_path.clone()))
                     .is_some()
             }
@@ -884,24 +703,27 @@ impl SettingsStore {
                     .remove(&(root_id, directory_path.clone()));
             }
             (LocalSettingsKind::Settings, Some(settings_contents)) => {
-                let new_settings =
-                    parse_json_with_comments::<Value>(settings_contents).map_err(|e| {
-                        InvalidSettingsError::LocalSettings {
-                            path: directory_path.join(local_settings_file_relative_path()),
-                            message: e.to_string(),
-                        }
-                    })?;
-                match self
-                    .raw_local_settings
-                    .entry((root_id, directory_path.clone()))
-                {
+                let new_settings = parse_json_with_comments::<ProjectSettingsContent>(
+                    settings_contents,
+                )
+                .map_err(|e| InvalidSettingsError::LocalSettings {
+                    path: directory_path.join(local_settings_file_relative_path()),
+                    message: e.to_string(),
+                })?;
+                match self.local_settings.entry((root_id, directory_path.clone())) {
                     btree_map::Entry::Vacant(v) => {
-                        v.insert(new_settings);
+                        v.insert(SettingsContent {
+                            project: new_settings,
+                            ..Default::default()
+                        });
                         zed_settings_changed = true;
                     }
                     btree_map::Entry::Occupied(mut o) => {
-                        if o.get() != &new_settings {
-                            o.insert(new_settings);
+                        if &o.get().project != &new_settings {
+                            o.insert(SettingsContent {
+                                project: new_settings,
+                                ..Default::default()
+                            });
                             zed_settings_changed = true;
                         }
                     }
@@ -953,17 +775,25 @@ impl SettingsStore {
         Ok(())
     }
 
-    pub fn set_extension_settings<T: Serialize>(&mut self, content: T, cx: &mut App) -> Result<()> {
-        let settings: Value = serde_json::to_value(content)?;
-        anyhow::ensure!(settings.is_object(), "settings must be an object");
-        self.raw_extension_settings = settings;
+    pub fn set_extension_settings(
+        &mut self,
+        content: ExtensionsSettingsContent,
+        cx: &mut App,
+    ) -> Result<()> {
+        self.extension_settings = Some(Box::new(SettingsContent {
+            project: ProjectSettingsContent {
+                all_languages: content.all_languages,
+                ..Default::default()
+            },
+            ..Default::default()
+        }));
         self.recompute_values(None, cx)?;
         Ok(())
     }
 
     /// Add or remove a set of local settings via a JSON string.
     pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut App) -> Result<()> {
-        self.raw_local_settings
+        self.local_settings
             .retain(|(worktree_id, _), _| worktree_id != &root_id);
         self.recompute_values(Some((root_id, "".as_ref())), cx)?;
         Ok(())
@@ -972,8 +802,8 @@ impl SettingsStore {
     pub fn local_settings(
         &self,
         root_id: WorktreeId,
-    ) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
-        self.raw_local_settings
+    ) -> impl '_ + Iterator<Item = (Arc<Path>, &ProjectSettingsContent)> {
+        self.local_settings
             .range(
                 (root_id, Path::new("").into())
                     ..(
@@ -981,7 +811,7 @@ impl SettingsStore {
                         Path::new("").into(),
                     ),
             )
-            .map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
+            .map(|((_, path), content)| (path.clone(), &content.project))
     }
 
     pub fn local_editorconfig_settings(
@@ -1005,190 +835,15 @@ impl SettingsStore {
         let mut generator = schemars::generate::SchemaSettings::draft2019_09()
             .with_transform(DefaultDenyUnknownFields)
             .into_generator();
-        let mut combined_schema = json!({
-            "type": "object",
-            "properties": {}
-        });
-
-        // Merge together settings schemas, similarly to json schema's "allOf". This merging is
-        // recursive, though at time of writing this recursive nature isn't used very much. An
-        // example of it is the schema for `jupyter` having contribution from both `EditorSettings`
-        // and `JupyterSettings`.
-        //
-        // This logic could be removed in favor of "allOf", but then there isn't the opportunity to
-        // validate and fully control the merge.
-        for setting_value in self.setting_values.values() {
-            let mut setting_schema = setting_value.json_schema(&mut generator);
-
-            if let Some(key) = setting_value.key() {
-                if let Some(properties) = combined_schema.get_mut("properties")
-                    && let Some(properties_obj) = properties.as_object_mut()
-                {
-                    if let Some(target) = properties_obj.get_mut(key) {
-                        merge_schema(target, setting_schema.to_value());
-                    } else {
-                        properties_obj.insert(key.to_string(), setting_schema.to_value());
-                    }
-                }
-            } else {
-                setting_schema.remove("description");
-                setting_schema.remove("additionalProperties");
-                merge_schema(&mut combined_schema, setting_schema.to_value());
-            }
-        }
 
-        fn merge_schema(target: &mut serde_json::Value, source: serde_json::Value) {
-            let (Some(target_obj), serde_json::Value::Object(source_obj)) =
-                (target.as_object_mut(), source)
-            else {
-                return;
-            };
-
-            for (source_key, source_value) in source_obj {
-                match source_key.as_str() {
-                    "properties" => {
-                        let serde_json::Value::Object(source_properties) = source_value else {
-                            log::error!(
-                                "bug: expected object for `{}` json schema field, but got: {}",
-                                source_key,
-                                source_value
-                            );
-                            continue;
-                        };
-                        let target_properties =
-                            target_obj.entry(source_key.clone()).or_insert(json!({}));
-                        let Some(target_properties) = target_properties.as_object_mut() else {
-                            log::error!(
-                                "bug: expected object for `{}` json schema field, but got: {}",
-                                source_key,
-                                target_properties
-                            );
-                            continue;
-                        };
-                        for (key, value) in source_properties {
-                            if let Some(existing) = target_properties.get_mut(&key) {
-                                merge_schema(existing, value);
-                            } else {
-                                target_properties.insert(key, value);
-                            }
-                        }
-                    }
-                    "allOf" | "anyOf" | "oneOf" => {
-                        let serde_json::Value::Array(source_array) = source_value else {
-                            log::error!(
-                                "bug: expected array for `{}` json schema field, but got: {}",
-                                source_key,
-                                source_value,
-                            );
-                            continue;
-                        };
-                        let target_array =
-                            target_obj.entry(source_key.clone()).or_insert(json!([]));
-                        let Some(target_array) = target_array.as_array_mut() else {
-                            log::error!(
-                                "bug: expected array for `{}` json schema field, but got: {}",
-                                source_key,
-                                target_array,
-                            );
-                            continue;
-                        };
-                        target_array.extend(source_array);
-                    }
-                    "type"
-                    | "$ref"
-                    | "enum"
-                    | "minimum"
-                    | "maximum"
-                    | "pattern"
-                    | "description"
-                    | "additionalProperties" => {
-                        if let Some(old_value) =
-                            target_obj.insert(source_key.clone(), source_value.clone())
-                            && old_value != source_value
-                        {
-                            log::error!(
-                                "bug: while merging JSON schemas, \
-                                    mismatch `\"{}\": {}` (before was `{}`)",
-                                source_key,
-                                old_value,
-                                source_value
-                            );
-                        }
-                    }
-                    _ => {
-                        log::error!(
-                            "bug: while merging settings JSON schemas, \
-                            encountered unexpected `\"{}\": {}`",
-                            source_key,
-                            source_value
-                        );
-                    }
-                }
-            }
-        }
+        let schema = UserSettingsContent::json_schema(&mut generator);
 
         // add schemas which are determined at runtime
         for parameterized_json_schema in inventory::iter::<ParameterizedJsonSchema>() {
             (parameterized_json_schema.add_and_get_ref)(&mut generator, schema_params, cx);
         }
 
-        // add merged settings schema to the definitions
-        const ZED_SETTINGS: &str = "ZedSettings";
-        let zed_settings_ref = add_new_subschema(&mut generator, ZED_SETTINGS, combined_schema);
-
-        // add `ZedSettingsOverride` which is the same as `ZedSettings` except that unknown
-        // fields are rejected. This is used for release stage settings and profiles.
-        let mut zed_settings_override = zed_settings_ref.clone();
-        zed_settings_override.insert("unevaluatedProperties".to_string(), false.into());
-        let zed_settings_override_ref = add_new_subschema(
-            &mut generator,
-            "ZedSettingsOverride",
-            zed_settings_override.to_value(),
-        );
-
-        // Remove `"additionalProperties": false` added by `DefaultDenyUnknownFields` so that
-        // unknown fields can be handled by the root schema and `ZedSettingsOverride`.
-        let mut definitions = generator.take_definitions(true);
-        definitions
-            .get_mut(ZED_SETTINGS)
-            .unwrap()
-            .as_object_mut()
-            .unwrap()
-            .remove("additionalProperties");
-
-        let meta_schema = generator
-            .settings()
-            .meta_schema
-            .as_ref()
-            .expect("meta_schema should be present in schemars settings")
-            .to_string();
-
-        json!({
-            "$schema": meta_schema,
-            "title": "Zed Settings",
-            "unevaluatedProperties": false,
-            // ZedSettings + settings overrides for each release stage / OS / profiles
-            "allOf": [
-                zed_settings_ref,
-                {
-                    "properties": {
-                        "dev": zed_settings_override_ref,
-                        "nightly": zed_settings_override_ref,
-                        "stable": zed_settings_override_ref,
-                        "preview": zed_settings_override_ref,
-                        "linux": zed_settings_override_ref,
-                        "macos": zed_settings_override_ref,
-                        "windows": zed_settings_override_ref,
-                        "profiles": {
-                            "type": "object",
-                            "description": "Configures any number of settings profiles.",
-                            "additionalProperties": zed_settings_override_ref
-                        }
-                    }
-                }
-            ],
-            "$defs": definitions,
-        })
+        schema.to_value()
     }
 
     fn recompute_values(
@@ -1197,90 +852,48 @@ impl SettingsStore {
         cx: &mut App,
     ) -> std::result::Result<(), InvalidSettingsError> {
         // Reload the global and local values for every setting.
-        let mut project_settings_stack = Vec::<DeserializedSetting>::new();
+        let mut project_settings_stack = Vec::<&SettingsContent>::new();
         let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new();
-        for setting_value in self.setting_values.values_mut() {
-            let default_settings = setting_value
-                .deserialize_setting(&self.raw_default_settings)
-                .map_err(|e| InvalidSettingsError::DefaultSettings {
-                    message: e.to_string(),
-                })?;
 
-            let global_settings = self
-                .raw_global_settings
-                .as_ref()
-                .and_then(|setting| setting_value.deserialize_setting(setting).log_err());
-
-            let extension_settings = setting_value
-                .deserialize_setting(&self.raw_extension_settings)
-                .log_err();
-
-            let user_settings = match setting_value.deserialize_setting(&self.raw_user_settings) {
-                Ok(settings) => Some(settings),
-                Err(error) => {
-                    return Err(InvalidSettingsError::UserSettings {
-                        message: error.to_string(),
-                    });
-                }
-            };
+        let mut refinements = Vec::default();
 
-            let server_settings = self
-                .raw_server_settings
-                .as_ref()
-                .and_then(|setting| setting_value.deserialize_setting(setting).log_err());
-
-            let mut release_channel_settings = None;
-            if let Some(release_settings) = &self
-                .raw_user_settings
-                .get(release_channel::RELEASE_CHANNEL.dev_name())
-                && let Some(release_settings) = setting_value
-                    .deserialize_setting(release_settings)
-                    .log_err()
-            {
-                release_channel_settings = Some(release_settings);
-            }
+        if let Some(extension_settings) = self.extension_settings.as_deref() {
+            refinements.push(extension_settings)
+        }
 
-            let mut os_settings = None;
-            if let Some(settings) = &self.raw_user_settings.get(env::consts::OS)
-                && let Some(settings) = setting_value.deserialize_setting(settings).log_err()
-            {
-                os_settings = Some(settings);
-            }
+        if let Some(global_settings) = self.global_settings.as_deref() {
+            refinements.push(global_settings)
+        }
 
-            let mut profile_settings = None;
-            if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>()
-                && let Some(profiles) = self.raw_user_settings.get("profiles")
-                && let Some(profile_json) = profiles.get(&active_profile.0)
-            {
-                profile_settings = setting_value.deserialize_setting(profile_json).log_err();
+        if let Some(user_settings) = self.user_settings.as_ref() {
+            refinements.push(&user_settings.content);
+            if let Some(release_channel) = user_settings.for_release_channel() {
+                refinements.push(release_channel)
+            }
+            if let Some(os) = user_settings.for_os() {
+                refinements.push(os)
+            }
+            if let Some(profile) = user_settings.for_profile(cx) {
+                refinements.push(profile)
             }
+        }
+
+        if let Some(server_settings) = self.server_settings.as_ref() {
+            refinements.push(server_settings)
+        }
 
+        for setting_value in self.setting_values.values_mut() {
             // If the global settings file changed, reload the global value for the field.
-            if changed_local_path.is_none()
-                && let Some(value) = setting_value
-                    .load_setting(
-                        SettingsSources {
-                            default: &default_settings,
-                            global: global_settings.as_ref(),
-                            extensions: extension_settings.as_ref(),
-                            user: user_settings.as_ref(),
-                            release_channel: release_channel_settings.as_ref(),
-                            operating_system: os_settings.as_ref(),
-                            profile: profile_settings.as_ref(),
-                            server: server_settings.as_ref(),
-                            project: &[],
-                        },
-                        cx,
-                    )
-                    .log_err()
-            {
+            if changed_local_path.is_none() {
+                let mut value = setting_value.from_default(&self.default_settings, cx);
+                setting_value.refine(value.as_mut(), &refinements, cx);
                 setting_value.set_global_value(value);
             }
 
             // Reload the local values for the setting.
             paths_stack.clear();
             project_settings_stack.clear();
-            for ((root_id, directory_path), local_settings) in &self.raw_local_settings {
+            for ((root_id, directory_path), local_settings) in &self.local_settings {
                 // Build a stack of all of the local values for that setting.
                 while let Some(prev_entry) = paths_stack.last() {
                     if let Some((prev_root_id, prev_path)) = prev_entry

crates/settings/src/vscode_import.rs πŸ”—

@@ -78,12 +78,7 @@ impl VsCodeSettings {
     }
 
     pub fn read_value(&self, setting: &str) -> Option<&Value> {
-        if let Some(value) = self.content.get(setting) {
-            return Some(value);
-        }
-        // TODO: maybe check if it's in [platform] settings for current platform as a fallback
-        // TODO: deal with language specific settings
-        None
+        self.content.get(setting)
     }
 
     pub fn read_string(&self, setting: &str) -> Option<&str> {
@@ -140,4 +135,8 @@ impl VsCodeSettings {
             *setting = Some(s)
         }
     }
+
+    pub fn read_enum<T>(&self, key: &str, f: impl FnOnce(&str) -> Option<T>) -> Option<T> {
+        self.content.get(key).and_then(Value::as_str).and_then(f)
+    }
 }

crates/settings_ui/Cargo.toml πŸ”—

@@ -1,42 +0,0 @@
-[package]
-name = "settings_ui"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/settings_ui.rs"
-
-[features]
-default = []
-test-support = []
-
-[dependencies]
-anyhow.workspace = true
-command_palette_hooks.workspace = true
-editor.workspace = true
-feature_flags.workspace = true
-gpui.workspace = true
-menu.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-settings.workspace = true
-smallvec.workspace = true
-theme.workspace = true
-ui.workspace = true
-workspace.workspace = true
-workspace-hack.workspace = true
-
-
-[dev-dependencies]
-debugger_ui.workspace = true
-
-# Uncomment other workspace dependencies as needed
-# assistant.workspace = true
-# client.workspace = true
-# project.workspace = true
-# settings.workspace = true

crates/settings_ui/src/appearance_settings_controls.rs πŸ”—

@@ -1,387 +0,0 @@
-use std::sync::Arc;
-
-use gpui::{App, FontFeatures, FontWeight};
-use settings::{EditableSettingControl, Settings};
-use theme::{
-    FontFamilyCache, FontFamilyName, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings,
-};
-use ui::{
-    CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
-    ToggleButton, prelude::*,
-};
-
-#[derive(IntoElement)]
-pub struct AppearanceSettingsControls {}
-
-impl AppearanceSettingsControls {
-    pub fn new() -> Self {
-        Self {}
-    }
-}
-
-impl RenderOnce for AppearanceSettingsControls {
-    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
-        SettingsContainer::new()
-            .child(
-                SettingsGroup::new("Theme").child(
-                    h_flex()
-                        .gap_2()
-                        .justify_between()
-                        .child(ThemeControl)
-                        .child(ThemeModeControl),
-                ),
-            )
-            .child(
-                SettingsGroup::new("Font")
-                    .child(
-                        h_flex()
-                            .gap_2()
-                            .justify_between()
-                            .child(UiFontFamilyControl)
-                            .child(UiFontWeightControl),
-                    )
-                    .child(UiFontSizeControl)
-                    .child(UiFontLigaturesControl),
-            )
-    }
-}
-
-#[derive(IntoElement)]
-struct ThemeControl;
-
-impl EditableSettingControl for ThemeControl {
-    type Value = String;
-    type Settings = ThemeSettings;
-
-    fn name(&self) -> SharedString {
-        "Theme".into()
-    }
-
-    fn read(cx: &App) -> Self::Value {
-        let settings = ThemeSettings::get_global(cx);
-        let appearance = SystemAppearance::global(cx);
-        settings
-            .theme_selection
-            .as_ref()
-            .map(|selection| selection.theme(appearance.0).to_string())
-            .unwrap_or_else(|| ThemeSettings::default_theme(*appearance).to_string())
-    }
-
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        cx: &App,
-    ) {
-        let appearance = SystemAppearance::global(cx);
-        settings.set_theme(value, appearance.0);
-    }
-}
-
-impl RenderOnce for ThemeControl {
-    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let value = Self::read(cx);
-
-        DropdownMenu::new(
-            "theme",
-            value,
-            ContextMenu::build(window, cx, |mut menu, _, cx| {
-                let theme_registry = ThemeRegistry::global(cx);
-
-                for theme in theme_registry.list_names() {
-                    menu = menu.custom_entry(
-                        {
-                            let theme = theme.clone();
-                            move |_window, _cx| Label::new(theme.clone()).into_any_element()
-                        },
-                        {
-                            let theme = theme.clone();
-                            move |_window, cx| {
-                                Self::write(theme.to_string(), cx);
-                            }
-                        },
-                    )
-                }
-
-                menu
-            }),
-        )
-        .full_width(true)
-    }
-}
-
-#[derive(IntoElement)]
-struct ThemeModeControl;
-
-impl EditableSettingControl for ThemeModeControl {
-    type Value = ThemeMode;
-    type Settings = ThemeSettings;
-
-    fn name(&self) -> SharedString {
-        "Theme Mode".into()
-    }
-
-    fn read(cx: &App) -> Self::Value {
-        let settings = ThemeSettings::get_global(cx);
-        settings
-            .theme_selection
-            .as_ref()
-            .and_then(|selection| selection.mode())
-            .unwrap_or_default()
-    }
-
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        settings.set_mode(value);
-    }
-}
-
-impl RenderOnce for ThemeModeControl {
-    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let value = Self::read(cx);
-
-        h_flex()
-            .child(
-                ToggleButton::new("light", "Light")
-                    .style(ButtonStyle::Filled)
-                    .size(ButtonSize::Large)
-                    .toggle_state(value == ThemeMode::Light)
-                    .on_click(|_, _, cx| Self::write(ThemeMode::Light, cx))
-                    .first(),
-            )
-            .child(
-                ToggleButton::new("system", "System")
-                    .style(ButtonStyle::Filled)
-                    .size(ButtonSize::Large)
-                    .toggle_state(value == ThemeMode::System)
-                    .on_click(|_, _, cx| Self::write(ThemeMode::System, cx))
-                    .middle(),
-            )
-            .child(
-                ToggleButton::new("dark", "Dark")
-                    .style(ButtonStyle::Filled)
-                    .size(ButtonSize::Large)
-                    .toggle_state(value == ThemeMode::Dark)
-                    .on_click(|_, _, cx| Self::write(ThemeMode::Dark, cx))
-                    .last(),
-            )
-    }
-}
-
-#[derive(IntoElement)]
-struct UiFontFamilyControl;
-
-impl EditableSettingControl for UiFontFamilyControl {
-    type Value = SharedString;
-    type Settings = ThemeSettings;
-
-    fn name(&self) -> SharedString {
-        "UI Font Family".into()
-    }
-
-    fn read(cx: &App) -> Self::Value {
-        let settings = ThemeSettings::get_global(cx);
-        settings.ui_font.family.clone()
-    }
-
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        settings.ui_font_family = Some(FontFamilyName(value.into()));
-    }
-}
-
-impl RenderOnce for UiFontFamilyControl {
-    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let value = Self::read(cx);
-
-        h_flex()
-            .gap_2()
-            .child(Icon::new(IconName::Font))
-            .child(DropdownMenu::new(
-                "ui-font-family",
-                value,
-                ContextMenu::build(window, cx, |mut menu, _, cx| {
-                    let font_family_cache = FontFamilyCache::global(cx);
-
-                    for font_name in font_family_cache.list_font_families(cx) {
-                        menu = menu.custom_entry(
-                            {
-                                let font_name = font_name.clone();
-                                move |_window, _cx| Label::new(font_name.clone()).into_any_element()
-                            },
-                            {
-                                let font_name = font_name.clone();
-                                move |_window, cx| {
-                                    Self::write(font_name.clone(), cx);
-                                }
-                            },
-                        )
-                    }
-
-                    menu
-                }),
-            ))
-    }
-}
-
-#[derive(IntoElement)]
-struct UiFontSizeControl;
-
-impl EditableSettingControl for UiFontSizeControl {
-    type Value = Pixels;
-    type Settings = ThemeSettings;
-
-    fn name(&self) -> SharedString {
-        "UI Font Size".into()
-    }
-
-    fn read(cx: &App) -> Self::Value {
-        ThemeSettings::get_global(cx).ui_font_size(cx)
-    }
-
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        settings.ui_font_size = Some(value.into());
-    }
-}
-
-impl RenderOnce for UiFontSizeControl {
-    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let value = Self::read(cx);
-
-        h_flex()
-            .gap_2()
-            .child(Icon::new(IconName::FontSize))
-            .child(NumericStepper::new(
-                "ui-font-size",
-                value.to_string(),
-                move |_, _, cx| {
-                    Self::write(value - px(1.), cx);
-                },
-                move |_, _, cx| {
-                    Self::write(value + px(1.), cx);
-                },
-            ))
-    }
-}
-
-#[derive(IntoElement)]
-struct UiFontWeightControl;
-
-impl EditableSettingControl for UiFontWeightControl {
-    type Value = FontWeight;
-    type Settings = ThemeSettings;
-
-    fn name(&self) -> SharedString {
-        "UI Font Weight".into()
-    }
-
-    fn read(cx: &App) -> Self::Value {
-        let settings = ThemeSettings::get_global(cx);
-        settings.ui_font.weight
-    }
-
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        settings.ui_font_weight = Some(value.0);
-    }
-}
-
-impl RenderOnce for UiFontWeightControl {
-    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let value = Self::read(cx);
-
-        h_flex()
-            .gap_2()
-            .child(Icon::new(IconName::FontWeight))
-            .child(DropdownMenu::new(
-                "ui-font-weight",
-                value.0.to_string(),
-                ContextMenu::build(window, cx, |mut menu, _window, _cx| {
-                    for weight in FontWeight::ALL {
-                        menu = menu.custom_entry(
-                            move |_window, _cx| Label::new(weight.0.to_string()).into_any_element(),
-                            {
-                                move |_window, cx| {
-                                    Self::write(weight, cx);
-                                }
-                            },
-                        )
-                    }
-
-                    menu
-                }),
-            ))
-    }
-}
-
-#[derive(IntoElement)]
-struct UiFontLigaturesControl;
-
-impl EditableSettingControl for UiFontLigaturesControl {
-    type Value = bool;
-    type Settings = ThemeSettings;
-
-    fn name(&self) -> SharedString {
-        "UI Font Ligatures".into()
-    }
-
-    fn read(cx: &App) -> Self::Value {
-        let settings = ThemeSettings::get_global(cx);
-        settings.ui_font.features.is_calt_enabled().unwrap_or(true)
-    }
-
-    fn apply(
-        settings: &mut <Self::Settings as Settings>::FileContent,
-        value: Self::Value,
-        _cx: &App,
-    ) {
-        let value = if value { 1 } else { 0 };
-
-        let mut features = settings
-            .ui_font_features
-            .as_ref()
-            .map(|features| features.tag_value_list().to_vec())
-            .unwrap_or_default();
-
-        if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
-            features[calt_index].1 = value;
-        } else {
-            features.push(("calt".into(), value));
-        }
-
-        settings.ui_font_features = Some(FontFeatures(Arc::new(features)));
-    }
-}
-
-impl RenderOnce for UiFontLigaturesControl {
-    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let value = Self::read(cx);
-
-        CheckboxWithLabel::new(
-            "ui-font-ligatures",
-            Label::new(self.name()),
-            value.into(),
-            |selection, _, cx| {
-                Self::write(
-                    match selection {
-                        ToggleState::Selected => true,
-                        ToggleState::Unselected | ToggleState::Indeterminate => false,
-                    },
-                    cx,
-                );
-            },
-        )
-    }
-}

crates/settings_ui/src/settings_ui.rs πŸ”—

@@ -1,1017 +0,0 @@
-mod appearance_settings_controls;
-
-use std::{
-    num::NonZeroU32,
-    ops::{Not, Range},
-    rc::Rc,
-};
-
-use anyhow::Context as _;
-use editor::{Editor, EditorSettingsControls};
-use feature_flags::{FeatureFlag, FeatureFlagAppExt};
-use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions};
-use settings::{
-    NumType, SettingsStore, SettingsUiEntry, SettingsUiEntryMetaData, SettingsUiItem,
-    SettingsUiItemDynamicMap, SettingsUiItemGroup, SettingsUiItemSingle, SettingsUiItemUnion,
-    SettingsValue,
-};
-use smallvec::SmallVec;
-use ui::{
-    ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple,
-    prelude::*,
-};
-use workspace::{
-    Workspace,
-    item::{Item, ItemEvent},
-};
-
-use crate::appearance_settings_controls::AppearanceSettingsControls;
-
-pub struct SettingsUiFeatureFlag;
-
-impl FeatureFlag for SettingsUiFeatureFlag {
-    const NAME: &'static str = "settings-ui";
-}
-
-actions!(
-    zed,
-    [
-        /// Opens settings UI.
-        OpenSettingsUi
-    ]
-);
-
-pub fn open_settings_editor(
-    workspace: &mut Workspace,
-    _: &OpenSettingsUi,
-    window: &mut Window,
-    cx: &mut Context<Workspace>,
-) {
-    // todo(settings_ui) open in a local workspace if this is remote.
-    let existing = workspace
-        .active_pane()
-        .read(cx)
-        .items()
-        .find_map(|item| item.downcast::<SettingsPage>());
-
-    if let Some(existing) = existing {
-        workspace.activate_item(&existing, true, true, window, cx);
-    } else {
-        let settings_page = SettingsPage::new(workspace, cx);
-        workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx)
-    }
-}
-
-pub fn init(cx: &mut App) {
-    cx.observe_new(|workspace: &mut Workspace, _, _| {
-        workspace.register_action_renderer(|div, _, _, cx| {
-            let settings_ui_actions = [std::any::TypeId::of::<OpenSettingsUi>()];
-            let has_flag = cx.has_flag::<SettingsUiFeatureFlag>();
-            command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| {
-                if has_flag {
-                    filter.show_action_types(&settings_ui_actions);
-                } else {
-                    filter.hide_action_types(&settings_ui_actions);
-                }
-            });
-            if has_flag {
-                div.on_action(cx.listener(open_settings_editor))
-            } else {
-                div
-            }
-        });
-    })
-    .detach();
-}
-
-pub struct SettingsPage {
-    focus_handle: FocusHandle,
-    settings_tree: SettingsUiTree,
-}
-
-impl SettingsPage {
-    pub fn new(_workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
-        cx.new(|cx| Self {
-            focus_handle: cx.focus_handle(),
-            settings_tree: SettingsUiTree::new(cx),
-        })
-    }
-}
-
-impl EventEmitter<ItemEvent> for SettingsPage {}
-
-impl Focusable for SettingsPage {
-    fn focus_handle(&self, _cx: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl Item for SettingsPage {
-    type Event = ItemEvent;
-
-    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
-        Some(Icon::new(IconName::Settings))
-    }
-
-    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
-        "Settings".into()
-    }
-
-    fn show_toolbar(&self) -> bool {
-        false
-    }
-
-    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
-        f(*event)
-    }
-}
-
-// We want to iterate over the side bar with root groups
-// - this is a loop over top level groups, and if any are expanded, recursively displaying their items
-// - Should be able to get all items from a group (flatten a group)
-// - Should be able to toggle/untoggle groups in UI (at least in sidebar)
-// - Search should be available
-//  - there should be an index of text -> item mappings, for using fuzzy::match
-//   - Do we want to show the parent groups when a item is matched?
-
-struct UiEntry {
-    title: SharedString,
-    path: Option<SharedString>,
-    documentation: Option<SharedString>,
-    _depth: usize,
-    // a
-    //  b     < a descendant range < a total descendant range
-    //    f   |                    |
-    //    g   |                    |
-    //  c     <                    |
-    //    d                        |
-    //    e                        <
-    descendant_range: Range<usize>,
-    total_descendant_range: Range<usize>,
-    next_sibling: Option<usize>,
-    // expanded: bool,
-    render: Option<SettingsUiItemSingle>,
-    dynamic_render: Option<SettingsUiItemUnion>,
-    generate_items: Option<(
-        SettingsUiItem,
-        fn(&serde_json::Value, &App) -> Vec<SettingsUiEntryMetaData>,
-        SmallVec<[SharedString; 1]>,
-    )>,
-}
-
-impl UiEntry {
-    fn first_descendant_index(&self) -> Option<usize> {
-        return self
-            .descendant_range
-            .is_empty()
-            .not()
-            .then_some(self.descendant_range.start);
-    }
-
-    fn nth_descendant_index(&self, tree: &[UiEntry], n: usize) -> Option<usize> {
-        let first_descendant_index = self.first_descendant_index()?;
-        let mut current_index = 0;
-        let mut current_descendant_index = Some(first_descendant_index);
-        while let Some(descendant_index) = current_descendant_index
-            && current_index < n
-        {
-            current_index += 1;
-            current_descendant_index = tree[descendant_index].next_sibling;
-        }
-        current_descendant_index
-    }
-}
-
-pub struct SettingsUiTree {
-    root_entry_indices: Vec<usize>,
-    entries: Vec<UiEntry>,
-    active_entry_index: usize,
-}
-
-fn build_tree_item(
-    tree: &mut Vec<UiEntry>,
-    entry: SettingsUiEntry,
-    depth: usize,
-    prev_index: Option<usize>,
-) {
-    // let tree: HashMap<Path, UiEntry>;
-    let index = tree.len();
-    tree.push(UiEntry {
-        title: entry.title.into(),
-        path: entry.path.map(SharedString::new_static),
-        documentation: entry.documentation.map(SharedString::new_static),
-        _depth: depth,
-        descendant_range: index + 1..index + 1,
-        total_descendant_range: index + 1..index + 1,
-        render: None,
-        next_sibling: None,
-        dynamic_render: None,
-        generate_items: None,
-    });
-    if let Some(prev_index) = prev_index {
-        tree[prev_index].next_sibling = Some(index);
-    }
-    match entry.item {
-        SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => {
-            for group_item in group_items {
-                let prev_index = tree[index]
-                    .descendant_range
-                    .is_empty()
-                    .not()
-                    .then_some(tree[index].descendant_range.end - 1);
-                tree[index].descendant_range.end = tree.len() + 1;
-                build_tree_item(tree, group_item, depth + 1, prev_index);
-                tree[index].total_descendant_range.end = tree.len();
-            }
-        }
-        SettingsUiItem::Single(item) => {
-            tree[index].render = Some(item);
-        }
-        SettingsUiItem::Union(dynamic_render) => {
-            // todo(settings_ui) take from item and store other fields instead of clone
-            // will also require replacing usage in render_recursive so it can know
-            // which options were actually rendered
-            let options = dynamic_render.options.clone();
-            tree[index].dynamic_render = Some(dynamic_render);
-            for option in options {
-                let Some(option) = option else { continue };
-                let prev_index = tree[index]
-                    .descendant_range
-                    .is_empty()
-                    .not()
-                    .then_some(tree[index].descendant_range.end - 1);
-                tree[index].descendant_range.end = tree.len() + 1;
-                build_tree_item(tree, option, depth + 1, prev_index);
-                tree[index].total_descendant_range.end = tree.len();
-            }
-        }
-        SettingsUiItem::DynamicMap(SettingsUiItemDynamicMap {
-            item: generate_settings_ui_item,
-            determine_items,
-            defaults_path,
-        }) => {
-            tree[index].generate_items = Some((
-                generate_settings_ui_item(),
-                determine_items,
-                defaults_path
-                    .into_iter()
-                    .copied()
-                    .map(SharedString::new_static)
-                    .collect(),
-            ));
-        }
-        SettingsUiItem::None => {
-            return;
-        }
-    }
-}
-
-impl SettingsUiTree {
-    pub fn new(cx: &App) -> Self {
-        let settings_store = SettingsStore::global(cx);
-        let mut tree = vec![];
-        let mut root_entry_indices = vec![];
-        for item in settings_store.settings_ui_items() {
-            if matches!(item.item, SettingsUiItem::None)
-            // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to
-            // link them to other groups
-            || matches!(item.item, SettingsUiItem::Single(_))
-            {
-                continue;
-            }
-
-            let prev_root_entry_index = root_entry_indices.last().copied();
-            root_entry_indices.push(tree.len());
-            build_tree_item(&mut tree, item, 0, prev_root_entry_index);
-        }
-
-        root_entry_indices.sort_by_key(|i| &tree[*i].title);
-
-        let active_entry_index = root_entry_indices[0];
-        Self {
-            entries: tree,
-            root_entry_indices,
-            active_entry_index,
-        }
-    }
-
-    // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree,
-    // so that we can keep none/skip and still test in CI that all settings have
-    #[cfg(feature = "test-support")]
-    pub fn all_paths(&self, cx: &App) -> Vec<Vec<SharedString>> {
-        fn all_paths_rec(
-            tree: &[UiEntry],
-            paths: &mut Vec<Vec<SharedString>>,
-            current_path: &mut Vec<SharedString>,
-            idx: usize,
-            cx: &App,
-        ) {
-            let child = &tree[idx];
-            let mut pushed_path = false;
-            if let Some(path) = child.path.as_ref() {
-                current_path.push(path.clone());
-                paths.push(current_path.clone());
-                pushed_path = true;
-            }
-            // todo(settings_ui): handle dynamic nodes here
-            let selected_descendant_index = child
-                .dynamic_render
-                .as_ref()
-                .map(|dynamic_render| {
-                    read_settings_value_from_path(
-                        SettingsStore::global(cx).raw_default_settings(),
-                        &current_path,
-                    )
-                    .map(|value| (dynamic_render.determine_option)(value, cx))
-                })
-                .and_then(|selected_descendant_index| {
-                    selected_descendant_index.map(|index| child.nth_descendant_index(tree, index))
-                });
-
-            if let Some(selected_descendant_index) = selected_descendant_index {
-                // just silently fail if we didn't find a setting value for the path
-                if let Some(descendant_index) = selected_descendant_index {
-                    all_paths_rec(tree, paths, current_path, descendant_index, cx);
-                }
-            } else if let Some(desc_idx) = child.first_descendant_index() {
-                let mut desc_idx = Some(desc_idx);
-                while let Some(descendant_index) = desc_idx {
-                    all_paths_rec(&tree, paths, current_path, descendant_index, cx);
-                    desc_idx = tree[descendant_index].next_sibling;
-                }
-            }
-            if pushed_path {
-                current_path.pop();
-            }
-        }
-
-        let mut paths = Vec::new();
-        for &index in &self.root_entry_indices {
-            all_paths_rec(&self.entries, &mut paths, &mut Vec::new(), index, cx);
-        }
-        paths
-    }
-}
-
-fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context<SettingsPage>) -> Div {
-    let mut nav = v_flex().p_4().gap_2();
-    for &index in &tree.root_entry_indices {
-        nav = nav.child(
-            div()
-                .id(index)
-                .on_click(cx.listener(move |settings, _, _, _| {
-                    settings.settings_tree.active_entry_index = index;
-                }))
-                .child(
-                    Label::new(tree.entries[index].title.clone())
-                        .size(LabelSize::Large)
-                        .when(tree.active_entry_index == index, |this| {
-                            this.color(Color::Selected)
-                        }),
-                ),
-        );
-    }
-    nav
-}
-
-fn render_content(
-    tree: &SettingsUiTree,
-    window: &mut Window,
-    cx: &mut Context<SettingsPage>,
-) -> Div {
-    let content = v_flex().size_full().gap_4();
-
-    let mut path = smallvec::smallvec![];
-
-    return render_recursive(
-        &tree.entries,
-        tree.active_entry_index,
-        &mut path,
-        content,
-        &mut None,
-        true,
-        window,
-        cx,
-    );
-}
-
-fn render_recursive(
-    tree: &[UiEntry],
-    index: usize,
-    path: &mut SmallVec<[SharedString; 1]>,
-    mut element: Div,
-    fallback_path: &mut Option<SmallVec<[SharedString; 1]>>,
-    render_next_title: bool,
-    window: &mut Window,
-    cx: &mut App,
-) -> Div {
-    let Some(child) = tree.get(index) else {
-        return element
-            .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error));
-    };
-
-    if render_next_title {
-        element = element.child(Label::new(child.title.clone()).size(LabelSize::Large));
-    }
-
-    // todo(settings_ui): subgroups?
-    let mut pushed_path = false;
-    if let Some(child_path) = child.path.as_ref() {
-        path.push(child_path.clone());
-        if let Some(fallback_path) = fallback_path.as_mut() {
-            fallback_path.push(child_path.clone());
-        }
-        pushed_path = true;
-    }
-    let settings_value = settings_value_from_settings_and_path(
-        path.clone(),
-        fallback_path.as_ref().map(|path| path.as_slice()),
-        child.title.clone(),
-        child.documentation.clone(),
-        // PERF: how to structure this better? There feels like there's a way to avoid the clone
-        // and every value lookup
-        SettingsStore::global(cx).raw_user_settings(),
-        SettingsStore::global(cx).raw_default_settings(),
-    );
-    if let Some(dynamic_render) = child.dynamic_render.as_ref() {
-        let value = settings_value.read();
-        let selected_index = (dynamic_render.determine_option)(value, cx);
-        element = element.child(div().child(render_toggle_button_group_inner(
-            settings_value.title.clone(),
-            dynamic_render.labels,
-            Some(selected_index),
-            {
-                let path = settings_value.path.clone();
-                let defaults = dynamic_render.defaults.clone();
-                move |idx, cx| {
-                    if idx == selected_index {
-                        return;
-                    }
-                    let default = defaults.get(idx).cloned().unwrap_or_default();
-                    SettingsValue::write_value(&path, default, cx);
-                }
-            },
-        )));
-        // we don't add descendants for unit options, so we adjust the selected index
-        // by the number of options we didn't add descendants for, to get the descendant index
-        let selected_descendant_index = selected_index
-            - dynamic_render.options[..selected_index]
-                .iter()
-                .filter(|option| option.is_none())
-                .count();
-        if dynamic_render.options[selected_index].is_some()
-            && let Some(descendant_index) =
-                child.nth_descendant_index(tree, selected_descendant_index)
-        {
-            element = render_recursive(
-                tree,
-                descendant_index,
-                path,
-                element,
-                fallback_path,
-                false,
-                window,
-                cx,
-            );
-        }
-    } else if let Some((settings_ui_item, generate_items, defaults_path)) =
-        child.generate_items.as_ref()
-    {
-        let generated_items = generate_items(settings_value.read(), cx);
-        let mut ui_items = Vec::with_capacity(generated_items.len());
-        for item in generated_items {
-            let settings_ui_entry = SettingsUiEntry {
-                path: None,
-                title: "",
-                documentation: None,
-                item: settings_ui_item.clone(),
-            };
-            let prev_index = if ui_items.is_empty() {
-                None
-            } else {
-                Some(ui_items.len() - 1)
-            };
-            let item_index = ui_items.len();
-            build_tree_item(
-                &mut ui_items,
-                settings_ui_entry,
-                child._depth + 1,
-                prev_index,
-            );
-            if item_index < ui_items.len() {
-                ui_items[item_index].path = None;
-                ui_items[item_index].title = item.title.clone();
-                ui_items[item_index].documentation = item.documentation.clone();
-
-                // push path instead of setting path on ui item so that the path isn't pushed to default_path as well
-                // when we recurse
-                path.push(item.path.clone());
-                element = render_recursive(
-                    &ui_items,
-                    item_index,
-                    path,
-                    element,
-                    &mut Some(defaults_path.clone()),
-                    true,
-                    window,
-                    cx,
-                );
-                path.pop();
-            }
-        }
-    } else if let Some(child_render) = child.render.as_ref() {
-        element = element.child(div().child(render_item_single(
-            settings_value,
-            child_render,
-            window,
-            cx,
-        )));
-    } else if let Some(child_index) = child.first_descendant_index() {
-        let mut index = Some(child_index);
-        while let Some(sub_child_index) = index {
-            element = render_recursive(
-                tree,
-                sub_child_index,
-                path,
-                element,
-                fallback_path,
-                true,
-                window,
-                cx,
-            );
-            index = tree[sub_child_index].next_sibling;
-        }
-    } else {
-        element = element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted)))
-    }
-
-    if pushed_path {
-        path.pop();
-        if let Some(fallback_path) = fallback_path.as_mut() {
-            fallback_path.pop();
-        }
-    }
-    return element;
-}
-
-impl Render for SettingsPage {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new());
-        div()
-            .grid()
-            .grid_cols(16)
-            .p_4()
-            .bg(cx.theme().colors().editor_background)
-            .size_full()
-            .child(
-                div()
-                    .id("settings-ui-nav")
-                    .col_span(2)
-                    .h_full()
-                    .child(render_nav(&self.settings_tree, window, cx)),
-            )
-            .child(
-                div().col_span(6).h_full().child(
-                    render_content(&self.settings_tree, window, cx)
-                        .id("settings-ui-content")
-                        .track_scroll(scroll_handle.read(cx))
-                        .overflow_y_scroll(),
-                ),
-            )
-    }
-}
-
-// todo(settings_ui): remove, only here as inspiration
-#[allow(dead_code)]
-fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement {
-    v_flex()
-        .p_4()
-        .size_full()
-        .gap_4()
-        .child(Label::new("Settings").size(LabelSize::Large))
-        .child(
-            v_flex().gap_1().child(Label::new("Appearance")).child(
-                v_flex()
-                    .elevation_2(cx)
-                    .child(AppearanceSettingsControls::new()),
-            ),
-        )
-        .child(
-            v_flex().gap_1().child(Label::new("Editor")).child(
-                v_flex()
-                    .elevation_2(cx)
-                    .child(EditorSettingsControls::new()),
-            ),
-        )
-}
-
-fn element_id_from_path(path: &[SharedString]) -> ElementId {
-    if path.len() == 0 {
-        panic!("Path length must not be zero");
-    } else if path.len() == 1 {
-        ElementId::Name(path[0].clone())
-    } else {
-        ElementId::from((
-            ElementId::from(path[path.len() - 2].clone()),
-            path[path.len() - 1].clone(),
-        ))
-    }
-}
-
-fn render_item_single(
-    settings_value: SettingsValue<serde_json::Value>,
-    item: &SettingsUiItemSingle,
-    window: &mut Window,
-    cx: &mut App,
-) -> AnyElement {
-    match item {
-        SettingsUiItemSingle::Custom(_) => div()
-            .child(format!("Item: {}", settings_value.path.join(".")))
-            .into_any_element(),
-        SettingsUiItemSingle::SwitchField => {
-            render_any_item(settings_value, render_switch_field, window, cx)
-        }
-        SettingsUiItemSingle::NumericStepper(num_type) => {
-            render_any_numeric_stepper(settings_value, *num_type, window, cx)
-        }
-        SettingsUiItemSingle::ToggleGroup {
-            variants: values,
-            labels: titles,
-        } => render_toggle_button_group(settings_value, values, titles, window, cx),
-        SettingsUiItemSingle::DropDown { variants, labels } => {
-            render_dropdown(settings_value, variants, labels, window, cx)
-        }
-        SettingsUiItemSingle::TextField => render_text_field(settings_value, window, cx),
-    }
-}
-
-pub fn read_settings_value_from_path<'a>(
-    settings_contents: &'a serde_json::Value,
-    path: &[impl AsRef<str>],
-) -> Option<&'a serde_json::Value> {
-    // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested
-    let Some((key, remaining)) = path.split_first() else {
-        return Some(settings_contents);
-    };
-    let Some(value) = settings_contents.get(key.as_ref()) else {
-        return None;
-    };
-
-    read_settings_value_from_path(value, remaining)
-}
-
-fn downcast_any_item<T: serde::de::DeserializeOwned>(
-    settings_value: SettingsValue<serde_json::Value>,
-) -> SettingsValue<T> {
-    let value = settings_value.value.map(|value| {
-        serde_json::from_value::<T>(value.clone())
-            .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
-            .with_context(|| format!("value is not a {}: {}", std::any::type_name::<T>(), value))
-            .unwrap()
-    });
-    // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values
-    let default_value = serde_json::from_value::<T>(settings_value.default_value)
-        .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
-        .with_context(|| format!("value is not a {}", std::any::type_name::<T>()))
-        .unwrap();
-    let deserialized_setting_value = SettingsValue {
-        title: settings_value.title,
-        path: settings_value.path,
-        documentation: settings_value.documentation,
-        value,
-        default_value,
-    };
-    deserialized_setting_value
-}
-
-fn render_any_item<T: serde::de::DeserializeOwned>(
-    settings_value: SettingsValue<serde_json::Value>,
-    render_fn: impl Fn(SettingsValue<T>, &mut Window, &mut App) -> AnyElement + 'static,
-    window: &mut Window,
-    cx: &mut App,
-) -> AnyElement {
-    let deserialized_setting_value = downcast_any_item(settings_value);
-    render_fn(deserialized_setting_value, window, cx)
-}
-
-fn render_any_numeric_stepper(
-    settings_value: SettingsValue<serde_json::Value>,
-    num_type: NumType,
-    window: &mut Window,
-    cx: &mut App,
-) -> AnyElement {
-    match num_type {
-        NumType::U64 => render_numeric_stepper::<u64>(
-            downcast_any_item(settings_value),
-            |n| u64::saturating_sub(n, 1),
-            |n| u64::saturating_add(n, 1),
-            |n| {
-                serde_json::Number::try_from(n)
-                    .context("Failed to convert u64 to serde_json::Number")
-            },
-            window,
-            cx,
-        ),
-        NumType::U32 => render_numeric_stepper::<u32>(
-            downcast_any_item(settings_value),
-            |n| u32::saturating_sub(n, 1),
-            |n| u32::saturating_add(n, 1),
-            |n| {
-                serde_json::Number::try_from(n)
-                    .context("Failed to convert u32 to serde_json::Number")
-            },
-            window,
-            cx,
-        ),
-        NumType::F32 => render_numeric_stepper::<f32>(
-            downcast_any_item(settings_value),
-            |a| a - 1.0,
-            |a| a + 1.0,
-            |n| {
-                serde_json::Number::from_f64(n as f64)
-                    .context("Failed to convert f32 to serde_json::Number")
-            },
-            window,
-            cx,
-        ),
-        NumType::USIZE => render_numeric_stepper::<usize>(
-            downcast_any_item(settings_value),
-            |n| usize::saturating_sub(n, 1),
-            |n| usize::saturating_add(n, 1),
-            |n| {
-                serde_json::Number::try_from(n)
-                    .context("Failed to convert usize to serde_json::Number")
-            },
-            window,
-            cx,
-        ),
-        NumType::U32NONZERO => render_numeric_stepper::<NonZeroU32>(
-            downcast_any_item(settings_value),
-            |a| NonZeroU32::new(u32::saturating_sub(a.get(), 1)).unwrap_or(NonZeroU32::MIN),
-            |a| NonZeroU32::new(u32::saturating_add(a.get(), 1)).unwrap_or(NonZeroU32::MAX),
-            |n| {
-                serde_json::Number::try_from(n.get())
-                    .context("Failed to convert usize to serde_json::Number")
-            },
-            window,
-            cx,
-        ),
-    }
-}
-
-fn render_numeric_stepper<T: serde::de::DeserializeOwned + std::fmt::Display + Copy + 'static>(
-    value: SettingsValue<T>,
-    saturating_sub_1: fn(T) -> T,
-    saturating_add_1: fn(T) -> T,
-    to_serde_number: fn(T) -> anyhow::Result<serde_json::Number>,
-    _window: &mut Window,
-    _cx: &mut App,
-) -> AnyElement {
-    let id = element_id_from_path(&value.path);
-    let path = value.path.clone();
-    let num = *value.read();
-
-    NumericStepper::new(
-        id,
-        num.to_string(),
-        {
-            let path = value.path;
-            move |_, _, cx| {
-                let Some(number) = to_serde_number(saturating_sub_1(num)).ok() else {
-                    return;
-                };
-                let new_value = serde_json::Value::Number(number);
-                SettingsValue::write_value(&path, new_value, cx);
-            }
-        },
-        move |_, _, cx| {
-            let Some(number) = to_serde_number(saturating_add_1(num)).ok() else {
-                return;
-            };
-
-            let new_value = serde_json::Value::Number(number);
-
-            SettingsValue::write_value(&path, new_value, cx);
-        },
-    )
-    .style(ui::NumericStepperStyle::Outlined)
-    .into_any_element()
-}
-
-fn render_switch_field(
-    value: SettingsValue<bool>,
-    _window: &mut Window,
-    _cx: &mut App,
-) -> AnyElement {
-    let id = element_id_from_path(&value.path);
-    let path = value.path.clone();
-    SwitchField::new(
-        id,
-        value.title.clone(),
-        value.documentation.clone(),
-        match value.read() {
-            true => ToggleState::Selected,
-            false => ToggleState::Unselected,
-        },
-        move |toggle_state, _, cx| {
-            let new_value = serde_json::Value::Bool(match toggle_state {
-                ToggleState::Indeterminate => {
-                    return;
-                }
-                ToggleState::Selected => true,
-                ToggleState::Unselected => false,
-            });
-
-            SettingsValue::write_value(&path, new_value, cx);
-        },
-    )
-    .into_any_element()
-}
-
-fn render_text_field(
-    value: SettingsValue<serde_json::Value>,
-    window: &mut Window,
-    cx: &mut App,
-) -> AnyElement {
-    let value = downcast_any_item::<String>(value);
-    let path = value.path.clone();
-    let editor = window.use_state(cx, {
-        let path = path.clone();
-        move |window, cx| {
-            let mut editor = Editor::single_line(window, cx);
-
-            cx.observe_global_in::<SettingsStore>(window, move |editor, window, cx| {
-                let user_settings = SettingsStore::global(cx).raw_user_settings();
-                if let Some(value) = read_settings_value_from_path(&user_settings, &path).cloned()
-                    && let Some(value) = value.as_str()
-                {
-                    editor.set_text(value, window, cx);
-                }
-            })
-            .detach();
-
-            editor.set_text(value.read().clone(), window, cx);
-            editor
-        }
-    });
-
-    let weak_editor = editor.downgrade();
-    let theme_colors = cx.theme().colors();
-
-    div()
-        .child(editor)
-        .bg(theme_colors.editor_background)
-        .border_1()
-        .rounded_lg()
-        .border_color(theme_colors.border)
-        .on_action::<menu::Confirm>({
-            move |_, _, cx| {
-                let new_value = weak_editor.read_with(cx, |editor, cx| editor.text(cx)).ok();
-
-                if let Some(new_value) = new_value {
-                    SettingsValue::write_value(&path, serde_json::Value::String(new_value), cx);
-                }
-            }
-        })
-        .into_any_element()
-}
-
-fn render_toggle_button_group(
-    value: SettingsValue<serde_json::Value>,
-    variants: &'static [&'static str],
-    labels: &'static [&'static str],
-    _: &mut Window,
-    _: &mut App,
-) -> AnyElement {
-    let value = downcast_any_item::<String>(value);
-    let active_value = value.read();
-    let selected_idx = variants.iter().position(|v| v == &active_value);
-
-    return render_toggle_button_group_inner(value.title, labels, selected_idx, {
-        let path = value.path.clone();
-        move |variant_index, cx| {
-            SettingsValue::write_value(
-                &path,
-                serde_json::Value::String(variants[variant_index].to_string()),
-                cx,
-            );
-        }
-    });
-}
-
-fn render_dropdown(
-    value: SettingsValue<serde_json::Value>,
-    variants: &'static [&'static str],
-    labels: &'static [&'static str],
-    window: &mut Window,
-    cx: &mut App,
-) -> AnyElement {
-    let value = downcast_any_item::<String>(value);
-    let id = element_id_from_path(&value.path);
-
-    let menu = window.use_keyed_state(id.clone(), cx, |window, cx| {
-        let path = value.path.clone();
-        let handler = Rc::new(move |variant: &'static str, cx: &mut App| {
-            SettingsValue::write_value(&path, serde_json::Value::String(variant.to_string()), cx);
-        });
-
-        ContextMenu::build(window, cx, |mut menu, _, _| {
-            for (label, variant) in labels.iter().zip(variants) {
-                menu = menu.entry(*label, None, {
-                    let handler = handler.clone();
-                    move |_, cx| {
-                        handler(variant, cx);
-                    }
-                });
-            }
-
-            menu
-        })
-    });
-
-    DropdownMenu::new(id, value.read(), menu.read(cx).clone())
-        .style(ui::DropdownStyle::Outlined)
-        .into_any_element()
-}
-
-fn render_toggle_button_group_inner(
-    title: SharedString,
-    labels: &'static [&'static str],
-    selected_idx: Option<usize>,
-    on_write: impl Fn(usize, &mut App) + 'static,
-) -> AnyElement {
-    fn make_toggle_group<const LEN: usize>(
-        title: SharedString,
-        selected_idx: Option<usize>,
-        on_write: Rc<dyn Fn(usize, &mut App)>,
-        labels: &'static [&'static str],
-    ) -> AnyElement {
-        let labels_array: [&'static str; LEN] = {
-            let mut arr = ["unused"; LEN];
-            arr.copy_from_slice(labels);
-            arr
-        };
-
-        let mut idx = 0;
-        ToggleButtonGroup::single_row(
-            title,
-            labels_array.map(|label| {
-                idx += 1;
-                let on_write = on_write.clone();
-                ToggleButtonSimple::new(label, move |_, _, cx| {
-                    on_write(idx - 1, cx);
-                })
-            }),
-        )
-        .when_some(selected_idx, |this, ix| this.selected_index(ix))
-        .style(ui::ToggleButtonGroupStyle::Filled)
-        .into_any_element()
-    }
-
-    let on_write = Rc::new(on_write);
-
-    macro_rules! templ_toggl_with_const_param {
-        ($len:expr) => {
-            if labels.len() == $len {
-                return make_toggle_group::<$len>(title.clone(), selected_idx, on_write, labels);
-            }
-        };
-    }
-    templ_toggl_with_const_param!(1);
-    templ_toggl_with_const_param!(2);
-    templ_toggl_with_const_param!(3);
-    templ_toggl_with_const_param!(4);
-    templ_toggl_with_const_param!(5);
-    templ_toggl_with_const_param!(6);
-    unreachable!("Too many variants");
-}
-
-fn settings_value_from_settings_and_path(
-    path: SmallVec<[SharedString; 1]>,
-    fallback_path: Option<&[SharedString]>,
-    title: SharedString,
-    documentation: Option<SharedString>,
-    user_settings: &serde_json::Value,
-    default_settings: &serde_json::Value,
-) -> SettingsValue<serde_json::Value> {
-    let default_value = read_settings_value_from_path(default_settings, &path)
-        .or_else(|| {
-            fallback_path.and_then(|fallback_path| {
-                read_settings_value_from_path(default_settings, fallback_path)
-            })
-        })
-        .with_context(|| format!("No default value for item at path {:?}", path.join(".")))
-        .expect("Default value set for item")
-        .clone();
-
-    let value = read_settings_value_from_path(user_settings, &path).cloned();
-    let settings_value = SettingsValue {
-        default_value,
-        value,
-        documentation,
-        path,
-        // todo(settings_ui) is title required inside SettingsValue?
-        title,
-    };
-    return settings_value;
-}

crates/terminal/src/terminal_settings.rs πŸ”—

@@ -2,22 +2,18 @@ use alacritty_terminal::vte::ansi::{
     CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
 };
 use collections::HashMap;
-use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, px};
+use gpui::{App, FontFallbacks, FontFeatures, FontWeight, Pixels, px};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
-use settings::{SettingsKey, SettingsSources, SettingsUi};
-use std::path::PathBuf;
+pub use settings::AlternateScroll;
+use settings::{
+    CursorShapeContent, SettingsContent, ShowScrollbar, TerminalBlink, TerminalDockPosition,
+    TerminalLineHeight, TerminalSettingsContent, VenvSettings, WorkingDirectory,
+};
 use task::Shell;
 use theme::FontFamilyName;
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum TerminalDockPosition {
-    Left,
-    Bottom,
-    Right,
-}
+use util::MergeFrom;
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 pub struct Toolbar {
@@ -28,7 +24,7 @@ pub struct Toolbar {
 pub struct TerminalSettings {
     pub shell: Shell,
     pub working_directory: WorkingDirectory,
-    pub font_size: Option<Pixels>,
+    pub font_size: Option<Pixels>, // todo(settings_refactor) can be non-optional...
     pub font_family: Option<FontFamilyName>,
     pub font_fallbacks: Option<FontFallbacks>,
     pub font_features: Option<FontFeatures>,
@@ -60,218 +56,135 @@ pub struct ScrollbarSettings {
     pub show: Option<ShowScrollbar>,
 }
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-pub struct ScrollbarSettingsContent {
-    /// When to show the scrollbar in the terminal.
-    ///
-    /// Default: inherits editor scrollbar settings
-    pub show: Option<Option<ShowScrollbar>>,
-}
-
-/// When to show the scrollbar in the terminal.
-///
-/// Default: auto
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowScrollbar {
-    /// Show the scrollbar if there's important information or
-    /// follow the system's configured behavior.
-    Auto,
-    /// Match the system's configured behavior.
-    System,
-    /// Always show the scrollbar.
-    Always,
-    /// Never show the scrollbar.
-    Never,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum VenvSettings {
-    #[default]
-    Off,
-    On {
-        /// Default directories to search for virtual environments, relative
-        /// to the current working directory. We recommend overriding this
-        /// in your project's settings, rather than globally.
-        activate_script: Option<ActivateScript>,
-        venv_name: Option<String>,
-        directories: Option<Vec<PathBuf>>,
-    },
-}
-
-pub struct VenvSettingsContent<'a> {
-    pub activate_script: ActivateScript,
-    pub venv_name: &'a str,
-    pub directories: &'a [PathBuf],
+fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell {
+    match shell {
+        settings::Shell::System => Shell::System,
+        settings::Shell::Program(program) => Shell::Program(program),
+        settings::Shell::WithArguments {
+            program,
+            args,
+            title_override,
+        } => Shell::WithArguments {
+            program,
+            args,
+            title_override,
+        },
+    }
 }
 
-impl VenvSettings {
-    pub fn as_option(&self) -> Option<VenvSettingsContent<'_>> {
-        match self {
-            VenvSettings::Off => None,
-            VenvSettings::On {
-                activate_script,
-                venv_name,
-                directories,
-            } => Some(VenvSettingsContent {
-                activate_script: activate_script.unwrap_or(ActivateScript::Default),
-                venv_name: venv_name.as_deref().unwrap_or(""),
-                directories: directories.as_deref().unwrap_or(&[]),
+impl settings::Settings for TerminalSettings {
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let content = content.terminal.clone().unwrap();
+        TerminalSettings {
+            shell: settings_shell_to_task_shell(content.shell.unwrap()),
+            working_directory: content.working_directory.unwrap(),
+            font_size: content.font_size.map(px),
+            font_family: content.font_family,
+            font_fallbacks: content.font_fallbacks.map(|fallbacks| {
+                FontFallbacks::from_fonts(
+                    fallbacks
+                        .into_iter()
+                        .map(|family| family.0.to_string())
+                        .collect(),
+                )
             }),
+            font_features: content.font_features,
+            font_weight: content.font_weight.map(FontWeight),
+            line_height: content.line_height.unwrap(),
+            env: content.env.unwrap(),
+            cursor_shape: content.cursor_shape.map(Into::into),
+            blinking: content.blinking.unwrap(),
+            alternate_scroll: content.alternate_scroll.unwrap(),
+            option_as_meta: content.option_as_meta.unwrap(),
+            copy_on_select: content.copy_on_select.unwrap(),
+            keep_selection_on_copy: content.keep_selection_on_copy.unwrap(),
+            button: content.button.unwrap(),
+            dock: content.dock.unwrap(),
+            default_width: px(content.default_width.unwrap()),
+            default_height: px(content.default_height.unwrap()),
+            detect_venv: content.detect_venv.unwrap(),
+            max_scroll_history_lines: content.max_scroll_history_lines,
+            toolbar: Toolbar {
+                breadcrumbs: content.toolbar.unwrap().breadcrumbs.unwrap(),
+            },
+            scrollbar: ScrollbarSettings {
+                show: content.scrollbar.unwrap().show.flatten(),
+            },
+            minimum_contrast: content.minimum_contrast.unwrap(),
         }
     }
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ActivateScript {
-    #[default]
-    Default,
-    Csh,
-    Fish,
-    Nushell,
-    PowerShell,
-    Pyenv,
-}
 
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(key = "terminal")]
-pub struct TerminalSettingsContent {
-    /// What shell to use when opening a terminal.
-    ///
-    /// Default: system
-    pub shell: Option<Shell>,
-    /// What working directory to use when launching the terminal
-    ///
-    /// Default: current_project_directory
-    pub working_directory: Option<WorkingDirectory>,
-    /// Sets the terminal's font size.
-    ///
-    /// If this option is not included,
-    /// the terminal will default to matching the buffer's font size.
-    pub font_size: Option<f32>,
-    /// Sets the terminal's font family.
-    ///
-    /// If this option is not included,
-    /// the terminal will default to matching the buffer's font family.
-    pub font_family: Option<FontFamilyName>,
-
-    /// Sets the terminal's font fallbacks.
-    ///
-    /// If this option is not included,
-    /// the terminal will default to matching the buffer's font fallbacks.
-    #[schemars(extend("uniqueItems" = true))]
-    pub font_fallbacks: Option<Vec<FontFamilyName>>,
-
-    /// Sets the terminal's line height.
-    ///
-    /// Default: comfortable
-    pub line_height: Option<TerminalLineHeight>,
-    pub font_features: Option<FontFeatures>,
-    /// Sets the terminal's font weight in CSS weight units 0-900.
-    pub font_weight: Option<f32>,
-    /// Any key-value pairs added to this list will be added to the terminal's
-    /// environment. Use `:` to separate multiple values.
-    ///
-    /// Default: {}
-    pub env: Option<HashMap<String, String>>,
-    /// Default cursor shape for the terminal.
-    /// Can be "bar", "block", "underline", or "hollow".
-    ///
-    /// Default: None
-    pub cursor_shape: Option<CursorShape>,
-    /// Sets the cursor blinking behavior in the terminal.
-    ///
-    /// Default: terminal_controlled
-    pub blinking: Option<TerminalBlink>,
-    /// Sets whether Alternate Scroll mode (code: ?1007) is active by default.
-    /// Alternate Scroll mode converts mouse scroll events into up / down key
-    /// presses when in the alternate screen (e.g. when running applications
-    /// like vim or  less). The terminal can still set and unset this mode.
-    ///
-    /// Default: on
-    pub alternate_scroll: Option<AlternateScroll>,
-    /// Sets whether the option key behaves as the meta key.
-    ///
-    /// Default: false
-    pub option_as_meta: Option<bool>,
-    /// Whether or not selecting text in the terminal will automatically
-    /// copy to the system clipboard.
-    ///
-    /// Default: false
-    pub copy_on_select: Option<bool>,
-    /// Whether to keep the text selection after copying it to the clipboard.
-    ///
-    /// Default: false
-    pub keep_selection_on_copy: Option<bool>,
-    /// Whether to show the terminal button in the status bar.
-    ///
-    /// Default: true
-    pub button: Option<bool>,
-    pub dock: Option<TerminalDockPosition>,
-    /// Default width when the terminal is docked to the left or right.
-    ///
-    /// Default: 640
-    pub default_width: Option<f32>,
-    /// Default height when the terminal is docked to the bottom.
-    ///
-    /// Default: 320
-    pub default_height: Option<f32>,
-    /// Activates the python virtual environment, if one is found, in the
-    /// terminal's working directory (as resolved by the working_directory
-    /// setting). Set this to "off" to disable this behavior.
-    ///
-    /// Default: on
-    pub detect_venv: Option<VenvSettings>,
-    /// The maximum number of lines to keep in the scrollback history.
-    /// Maximum allowed value is 100_000, all values above that will be treated as 100_000.
-    /// 0 disables the scrolling.
-    /// Existing terminals will not pick up this change until they are recreated.
-    /// See <a href="https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213">Alacritty documentation</a> for more information.
-    ///
-    /// Default: 10_000
-    pub max_scroll_history_lines: Option<usize>,
-    /// Toolbar related settings
-    pub toolbar: Option<ToolbarContent>,
-    /// Scrollbar-related settings
-    pub scrollbar: Option<ScrollbarSettingsContent>,
-    /// The minimum APCA perceptual contrast between foreground and background colors.
-    ///
-    /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
-    /// especially for dark mode. Values range from 0 to 106.
-    ///
-    /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
-    /// https://readtech.org/ARC/tests/bronze-simple-mode/
-    /// - 0: No contrast adjustment
-    /// - 45: Minimum for large fluent text (36px+)
-    /// - 60: Minimum for other content text
-    /// - 75: Minimum for body text
-    /// - 90: Preferred for body text
-    ///
-    /// Default: 45
-    pub minimum_contrast: Option<f32>,
-}
-
-impl settings::Settings for TerminalSettings {
-    type FileContent = TerminalSettingsContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
-        let settings: Self = sources.json_merge()?;
-
-        // Validate minimum_contrast for APCA
-        if settings.minimum_contrast < 0.0 || settings.minimum_contrast > 106.0 {
-            anyhow::bail!(
-                "terminal.minimum_contrast must be between 0 and 106, but got {}. \
-                APCA values: 0 = no adjustment, 75 = recommended for body text, 106 = maximum contrast.",
-                settings.minimum_contrast
-            );
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(content) = &content.terminal else {
+            return;
+        };
+        self.shell
+            .merge_from(&content.shell.clone().map(settings_shell_to_task_shell));
+        self.working_directory
+            .merge_from(&content.working_directory);
+        if let Some(font_size) = content.font_size.map(px) {
+            self.font_size = Some(font_size)
         }
-
-        Ok(settings)
+        if let Some(font_family) = content.font_family.clone() {
+            self.font_family = Some(font_family);
+        }
+        if let Some(fallbacks) = content.font_fallbacks.clone() {
+            self.font_fallbacks = Some(FontFallbacks::from_fonts(
+                fallbacks
+                    .into_iter()
+                    .map(|family| family.0.to_string())
+                    .collect(),
+            ))
+        }
+        if let Some(font_features) = content.font_features.clone() {
+            self.font_features = Some(font_features)
+        }
+        if let Some(font_weight) = content.font_weight {
+            self.font_weight = Some(FontWeight(font_weight));
+        }
+        self.line_height.merge_from(&content.line_height);
+        if let Some(env) = &content.env {
+            for (key, value) in env {
+                self.env.insert(key.clone(), value.clone());
+            }
+        }
+        if let Some(cursor_shape) = content.cursor_shape {
+            self.cursor_shape = Some(cursor_shape.into())
+        }
+        self.blinking.merge_from(&content.blinking);
+        self.alternate_scroll.merge_from(&content.alternate_scroll);
+        self.option_as_meta.merge_from(&content.option_as_meta);
+        self.copy_on_select.merge_from(&content.copy_on_select);
+        self.keep_selection_on_copy
+            .merge_from(&content.keep_selection_on_copy);
+        self.button.merge_from(&content.button);
+        self.dock.merge_from(&content.dock);
+        self.default_width
+            .merge_from(&content.default_width.map(px));
+        self.default_height
+            .merge_from(&content.default_height.map(px));
+        self.detect_venv.merge_from(&content.detect_venv);
+        if let Some(max_scroll_history_lines) = content.max_scroll_history_lines {
+            self.max_scroll_history_lines = Some(max_scroll_history_lines)
+        }
+        self.toolbar.breadcrumbs.merge_from(
+            &content
+                .toolbar
+                .as_ref()
+                .and_then(|toolbar| toolbar.breadcrumbs),
+        );
+        self.scrollbar.show.merge_from(
+            &content
+                .scrollbar
+                .as_ref()
+                .and_then(|scrollbar| scrollbar.show),
+        );
+        self.minimum_contrast.merge_from(&content.minimum_contrast);
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
+    fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) {
+        let mut default = TerminalSettingsContent::default();
+        let current = content.terminal.as_mut().unwrap_or(&mut default);
         let name = |s| format!("terminal.integrated.{s}");
 
         vscode.f32_setting(&name("fontSize"), &mut current.font_size);
@@ -290,9 +203,9 @@ impl settings::Settings for TerminalSettings {
             &name("cursorStyle"),
             &mut current.cursor_shape,
             |s| match s {
-                "block" => Some(CursorShape::Block),
-                "line" => Some(CursorShape::Bar),
-                "underline" => Some(CursorShape::Underline),
+                "block" => Some(CursorShapeContent::Block),
+                "line" => Some(CursorShapeContent::Bar),
+                "underline" => Some(CursorShapeContent::Underline),
                 _ => None,
             },
         );
@@ -316,7 +229,7 @@ impl settings::Settings for TerminalSettings {
         // TODO: handle arguments
         let shell_name = format!("{platform}Exec");
         if let Some(s) = vscode.read_string(&name(&shell_name)) {
-            current.shell = Some(Shell::Program(s.to_owned()))
+            current.shell = Some(settings::Shell::Program(s.to_owned()))
         }
 
         if let Some(env) = vscode
@@ -337,81 +250,12 @@ impl settings::Settings for TerminalSettings {
                 }
             }
         }
+        if content.terminal.is_none() && default != TerminalSettingsContent::default() {
+            content.terminal = Some(default)
+        }
     }
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
-#[serde(rename_all = "snake_case")]
-pub enum TerminalLineHeight {
-    /// Use a line height that's comfortable for reading, 1.618
-    #[default]
-    Comfortable,
-    /// Use a standard line height, 1.3. This option is useful for TUIs,
-    /// particularly if they use box characters
-    Standard,
-    /// Use a custom line height.
-    Custom(f32),
-}
-
-impl TerminalLineHeight {
-    pub fn value(&self) -> AbsoluteLength {
-        let value = match self {
-            TerminalLineHeight::Comfortable => 1.618,
-            TerminalLineHeight::Standard => 1.3,
-            TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.),
-        };
-        px(value).into()
-    }
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum TerminalBlink {
-    /// Never blink the cursor, ignoring the terminal mode.
-    Off,
-    /// Default the cursor blink to off, but allow the terminal to
-    /// set blinking.
-    TerminalControlled,
-    /// Always blink the cursor, ignoring the terminal mode.
-    On,
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum AlternateScroll {
-    On,
-    Off,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum WorkingDirectory {
-    /// Use the current file's project directory.  Will Fallback to the
-    /// first project directory strategy if unsuccessful.
-    CurrentProjectDirectory,
-    /// Use the first project in this workspace's directory.
-    FirstProjectDirectory,
-    /// Always use this platform's home directory (if it can be found).
-    AlwaysHome,
-    /// Always use a specific directory. This value will be shell expanded.
-    /// If this path is not a valid directory the terminal will default to
-    /// this platform's home directory  (if it can be found).
-    Always { directory: String },
-}
-
-// Toolbar related settings
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-pub struct ToolbarContent {
-    /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
-    /// Only shown if the terminal title is not empty.
-    ///
-    /// The shell running in the terminal needs to be configured to emit the title.
-    /// Example: `echo -e "\e]2;New Title\007";`
-    ///
-    /// Default: true
-    pub breadcrumbs: Option<bool>,
-}
-
 #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum CursorShape {
@@ -426,6 +270,17 @@ pub enum CursorShape {
     Hollow,
 }
 
+impl From<settings::CursorShapeContent> for CursorShape {
+    fn from(value: settings::CursorShapeContent) -> Self {
+        match value {
+            settings::CursorShapeContent::Block => CursorShape::Block,
+            settings::CursorShapeContent::Underline => CursorShape::Underline,
+            settings::CursorShapeContent::Bar => CursorShape::Bar,
+            settings::CursorShapeContent::Hollow => CursorShape::Hollow,
+        }
+    }
+}
+
 impl From<CursorShape> for AlacCursorShape {
     fn from(value: CursorShape) -> Self {
         match value {

crates/terminal_view/src/terminal_panel.rs πŸ”—

@@ -18,12 +18,9 @@ use gpui::{
 use itertools::Itertools;
 use project::{Fs, Project, ProjectEntryId};
 use search::{BufferSearchBar, buffer_search::DivRegistrar};
-use settings::Settings;
+use settings::{Settings, TerminalDockPosition};
 use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId};
-use terminal::{
-    Terminal,
-    terminal_settings::{TerminalDockPosition, TerminalSettings},
-};
+use terminal::{Terminal, terminal_settings::TerminalSettings};
 use ui::{
     ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable, Tooltip,
     prelude::*,
@@ -1465,18 +1462,14 @@ impl Panel for TerminalPanel {
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        settings::update_settings_file::<TerminalSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings, _| {
-                let dock = match position {
-                    DockPosition::Left => TerminalDockPosition::Left,
-                    DockPosition::Bottom => TerminalDockPosition::Bottom,
-                    DockPosition::Right => TerminalDockPosition::Right,
-                };
-                settings.dock = Some(dock);
-            },
-        );
+        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            let dock = match position {
+                DockPosition::Left => TerminalDockPosition::Left,
+                DockPosition::Bottom => TerminalDockPosition::Bottom,
+                DockPosition::Right => TerminalDockPosition::Right,
+            };
+            settings.terminal.get_or_insert_default().dock = Some(dock);
+        });
     }
 
     fn size(&self, window: &Window, cx: &App) -> Pixels {

crates/terminal_view/src/terminal_view.rs πŸ”—

@@ -25,7 +25,7 @@ use terminal::{
         index::Point,
         term::{TermMode, point_to_viewport, search::RegexSearch},
     },
-    terminal_settings::{self, CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory},
+    terminal_settings::{CursorShape, TerminalSettings},
 };
 use terminal_element::TerminalElement;
 use terminal_panel::TerminalPanel;
@@ -50,7 +50,7 @@ use workspace::{
 };
 
 use serde::Deserialize;
-use settings::{Settings, SettingsStore};
+use settings::{Settings, SettingsStore, TerminalBlink, WorkingDirectory};
 use smol::Timer;
 use zed_actions::assistant::InlineAssist;
 
@@ -997,12 +997,7 @@ impl ScrollbarVisibility for TerminalScrollbarSettingsWrapper {
         TerminalSettings::get_global(cx)
             .scrollbar
             .show
-            .map(|value| match value {
-                terminal_settings::ShowScrollbar::Auto => scrollbars::ShowScrollbar::Auto,
-                terminal_settings::ShowScrollbar::System => scrollbars::ShowScrollbar::System,
-                terminal_settings::ShowScrollbar::Always => scrollbars::ShowScrollbar::Always,
-                terminal_settings::ShowScrollbar::Never => scrollbars::ShowScrollbar::Never,
-            })
+            .map(Into::into)
             .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show)
     }
 }

crates/theme/Cargo.toml πŸ”—

@@ -33,7 +33,6 @@ schemars = { workspace = true, features = ["indexmap2"] }
 serde.workspace = true
 serde_json.workspace = true
 serde_json_lenient.workspace = true
-serde_repr.workspace = true
 settings.workspace = true
 strum.workspace = true
 thiserror.workspace = true

crates/theme/src/schema.rs πŸ”—

@@ -1,31 +1,15 @@
 #![allow(missing_docs)]
 
-use anyhow::Result;
-use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance};
+use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla};
 use indexmap::IndexMap;
 use palette::FromColor;
-use schemars::{JsonSchema, JsonSchema_repr};
-use serde::{Deserialize, Deserializer, Serialize};
-use serde_json::Value;
-use serde_repr::{Deserialize_repr, Serialize_repr};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::{AccentContent, PlayerColorContent};
+pub use settings::{FontWeightContent, WindowBackgroundContent};
 
 use crate::{StatusColorsRefinement, ThemeColorsRefinement};
 
-pub(crate) fn try_parse_color(color: &str) -> Result<Hsla> {
-    let rgba = gpui::Rgba::try_from(color)?;
-    let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a));
-    let hsla = palette::Hsla::from_color(rgba);
-
-    let hsla = gpui::hsla(
-        hsla.hue.into_positive_degrees() / 360.,
-        hsla.saturation,
-        hsla.lightness,
-        hsla.alpha,
-    );
-
-    Ok(hsla)
-}
-
 fn ensure_non_opaque(color: Hsla) -> Hsla {
     const MAXIMUM_OPACITY: f32 = 0.7;
     if color.a <= MAXIMUM_OPACITY {
@@ -49,25 +33,6 @@ pub enum AppearanceContent {
     Dark,
 }
 
-/// The background appearance of the window.
-#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum WindowBackgroundContent {
-    Opaque,
-    Transparent,
-    Blurred,
-}
-
-impl From<WindowBackgroundContent> for WindowBackgroundAppearance {
-    fn from(value: WindowBackgroundContent) -> Self {
-        match value {
-            WindowBackgroundContent::Opaque => WindowBackgroundAppearance::Opaque,
-            WindowBackgroundContent::Transparent => WindowBackgroundAppearance::Transparent,
-            WindowBackgroundContent::Blurred => WindowBackgroundAppearance::Blurred,
-        }
-    }
-}
-
 /// The content of a serialized theme family.
 #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
 pub struct ThemeFamilyContent {
@@ -81,7 +46,7 @@ pub struct ThemeFamilyContent {
 pub struct ThemeContent {
     pub name: String,
     pub appearance: AppearanceContent,
-    pub style: ThemeStyleContent,
+    pub style: settings::ThemeStyleContent,
 }
 
 /// The content of a serialized theme.
@@ -89,1494 +54,748 @@ pub struct ThemeContent {
 #[serde(default)]
 pub struct ThemeStyleContent {
     #[serde(default, rename = "background.appearance")]
-    pub window_background_appearance: Option<WindowBackgroundContent>,
+    pub window_background_appearance: Option<settings::WindowBackgroundContent>,
 
     #[serde(default)]
     pub accents: Vec<AccentContent>,
 
     #[serde(flatten, default)]
-    pub colors: ThemeColorsContent,
+    pub colors: settings::ThemeColorsContent,
 
     #[serde(flatten, default)]
-    pub status: StatusColorsContent,
+    pub status: settings::StatusColorsContent,
 
     #[serde(default)]
     pub players: Vec<PlayerColorContent>,
 
     /// The styles for syntax nodes.
     #[serde(default)]
-    pub syntax: IndexMap<String, HighlightStyleContent>,
+    pub syntax: IndexMap<String, settings::HighlightStyleContent>,
 }
 
-impl ThemeStyleContent {
-    /// Returns a [`ThemeColorsRefinement`] based on the colors in the [`ThemeContent`].
-    #[inline(always)]
-    pub fn theme_colors_refinement(&self) -> ThemeColorsRefinement {
-        self.colors
-            .theme_colors_refinement(&self.status_colors_refinement())
-    }
-
-    /// Returns a [`StatusColorsRefinement`] based on the colors in the [`ThemeContent`].
-    #[inline(always)]
-    pub fn status_colors_refinement(&self) -> StatusColorsRefinement {
-        self.status.status_colors_refinement()
-    }
-
-    /// Returns the syntax style overrides in the [`ThemeContent`].
-    pub fn syntax_overrides(&self) -> Vec<(String, HighlightStyle)> {
-        self.syntax
-            .iter()
-            .map(|(key, style)| {
-                (
-                    key.clone(),
-                    HighlightStyle {
-                        color: style
-                            .color
-                            .as_ref()
-                            .and_then(|color| try_parse_color(color).ok()),
-                        background_color: style
-                            .background_color
-                            .as_ref()
-                            .and_then(|color| try_parse_color(color).ok()),
-                        font_style: style.font_style.map(FontStyle::from),
-                        font_weight: style.font_weight.map(FontWeight::from),
-                        ..Default::default()
-                    },
-                )
-            })
-            .collect()
-    }
+/// Returns the syntax style overrides in the [`ThemeContent`].
+pub fn syntax_overrides(this: &settings::ThemeStyleContent) -> Vec<(String, HighlightStyle)> {
+    this.syntax
+        .iter()
+        .map(|(key, style)| {
+            (
+                key.clone(),
+                HighlightStyle {
+                    color: style
+                        .color
+                        .as_ref()
+                        .and_then(|color| try_parse_color(color).ok()),
+                    background_color: style
+                        .background_color
+                        .as_ref()
+                        .and_then(|color| try_parse_color(color).ok()),
+                    font_style: style.font_style.map(FontStyle::from),
+                    font_weight: style.font_weight.map(FontWeight::from),
+                    ..Default::default()
+                },
+            )
+        })
+        .collect()
 }
 
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
-#[serde(default)]
-pub struct ThemeColorsContent {
-    /// Border color. Used for most borders, is usually a high contrast color.
-    #[serde(rename = "border")]
-    pub border: Option<String>,
-
-    /// Border color. Used for deemphasized borders, like a visual divider between two sections
-    #[serde(rename = "border.variant")]
-    pub border_variant: Option<String>,
-
-    /// Border color. Used for focused elements, like keyboard focused list item.
-    #[serde(rename = "border.focused")]
-    pub border_focused: Option<String>,
-
-    /// Border color. Used for selected elements, like an active search filter or selected checkbox.
-    #[serde(rename = "border.selected")]
-    pub border_selected: Option<String>,
-
-    /// Border color. Used for transparent borders. Used for placeholder borders when an element gains a border on state change.
-    #[serde(rename = "border.transparent")]
-    pub border_transparent: Option<String>,
-
-    /// Border color. Used for disabled elements, like a disabled input or button.
-    #[serde(rename = "border.disabled")]
-    pub border_disabled: Option<String>,
-
-    /// Background color. Used for elevated surfaces, like a context menu, popup, or dialog.
-    #[serde(rename = "elevated_surface.background")]
-    pub elevated_surface_background: Option<String>,
-
-    /// Background Color. Used for grounded surfaces like a panel or tab.
-    #[serde(rename = "surface.background")]
-    pub surface_background: Option<String>,
-
-    /// Background Color. Used for the app background and blank panels or windows.
-    #[serde(rename = "background")]
-    pub background: Option<String>,
-
-    /// Background Color. Used for the background of an element that should have a different background than the surface it's on.
-    ///
-    /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons...
-    ///
-    /// For an element that should have the same background as the surface it's on, use `ghost_element_background`.
-    #[serde(rename = "element.background")]
-    pub element_background: Option<String>,
-
-    /// Background Color. Used for the hover state of an element that should have a different background than the surface it's on.
-    ///
-    /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen.
-    #[serde(rename = "element.hover")]
-    pub element_hover: Option<String>,
-
-    /// Background Color. Used for the active state of an element that should have a different background than the surface it's on.
-    ///
-    /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed.
-    #[serde(rename = "element.active")]
-    pub element_active: Option<String>,
-
-    /// Background Color. Used for the selected state of an element that should have a different background than the surface it's on.
-    ///
-    /// Selected states are triggered by the element being selected (or "activated") by the user.
-    ///
-    /// This could include a selected checkbox, a toggleable button that is toggled on, etc.
-    #[serde(rename = "element.selected")]
-    pub element_selected: Option<String>,
-
-    /// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on.
-    ///
-    /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input.
-    #[serde(rename = "element.disabled")]
-    pub element_disabled: Option<String>,
-
-    /// Background Color. Used for the background of selections in a UI element.
-    #[serde(rename = "element.selection_background")]
-    pub element_selection_background: Option<String>,
-
-    /// Background Color. Used for the area that shows where a dragged element will be dropped.
-    #[serde(rename = "drop_target.background")]
-    pub drop_target_background: Option<String>,
-
-    /// Border Color. Used for the border that shows where a dragged element will be dropped.
-    #[serde(rename = "drop_target.border")]
-    pub drop_target_border: Option<String>,
-
-    /// Used for the background of a ghost element that should have the same background as the surface it's on.
-    ///
-    /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons...
-    ///
-    /// For an element that should have a different background than the surface it's on, use `element_background`.
-    #[serde(rename = "ghost_element.background")]
-    pub ghost_element_background: Option<String>,
-
-    /// Background Color. Used for the hover state of a ghost element that should have the same background as the surface it's on.
-    ///
-    /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen.
-    #[serde(rename = "ghost_element.hover")]
-    pub ghost_element_hover: Option<String>,
-
-    /// Background Color. Used for the active state of a ghost element that should have the same background as the surface it's on.
-    ///
-    /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed.
-    #[serde(rename = "ghost_element.active")]
-    pub ghost_element_active: Option<String>,
-
-    /// Background Color. Used for the selected state of a ghost element that should have the same background as the surface it's on.
-    ///
-    /// Selected states are triggered by the element being selected (or "activated") by the user.
-    ///
-    /// This could include a selected checkbox, a toggleable button that is toggled on, etc.
-    #[serde(rename = "ghost_element.selected")]
-    pub ghost_element_selected: Option<String>,
-
-    /// Background Color. Used for the disabled state of a ghost element that should have the same background as the surface it's on.
-    ///
-    /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input.
-    #[serde(rename = "ghost_element.disabled")]
-    pub ghost_element_disabled: Option<String>,
-
-    /// Text Color. Default text color used for most text.
-    #[serde(rename = "text")]
-    pub text: Option<String>,
-
-    /// Text Color. Color of muted or deemphasized text. It is a subdued version of the standard text color.
-    #[serde(rename = "text.muted")]
-    pub text_muted: Option<String>,
-
-    /// Text Color. Color of the placeholder text typically shown in input fields to guide the user to enter valid data.
-    #[serde(rename = "text.placeholder")]
-    pub text_placeholder: Option<String>,
-
-    /// Text Color. Color used for text denoting disabled elements. Typically, the color is faded or grayed out to emphasize the disabled state.
-    #[serde(rename = "text.disabled")]
-    pub text_disabled: Option<String>,
-
-    /// Text Color. Color used for emphasis or highlighting certain text, like an active filter or a matched character in a search.
-    #[serde(rename = "text.accent")]
-    pub text_accent: Option<String>,
-
-    /// Fill Color. Used for the default fill color of an icon.
-    #[serde(rename = "icon")]
-    pub icon: Option<String>,
-
-    /// Fill Color. Used for the muted or deemphasized fill color of an icon.
-    ///
-    /// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight.
-    #[serde(rename = "icon.muted")]
-    pub icon_muted: Option<String>,
-
-    /// Fill Color. Used for the disabled fill color of an icon.
-    ///
-    /// Disabled states are shown when a user cannot interact with an element, like a icon button.
-    #[serde(rename = "icon.disabled")]
-    pub icon_disabled: Option<String>,
-
-    /// Fill Color. Used for the placeholder fill color of an icon.
-    ///
-    /// This might be used to show an icon in an input that disappears when the user enters text.
-    #[serde(rename = "icon.placeholder")]
-    pub icon_placeholder: Option<String>,
-
-    /// Fill Color. Used for the accent fill color of an icon.
-    ///
-    /// This might be used to show when a toggleable icon button is selected.
-    #[serde(rename = "icon.accent")]
-    pub icon_accent: Option<String>,
-
-    /// Color used to accent some of the debuggers elements
-    /// Only accent breakpoint & breakpoint related symbols right now
-    #[serde(rename = "debugger.accent")]
-    pub debugger_accent: Option<String>,
-
-    #[serde(rename = "status_bar.background")]
-    pub status_bar_background: Option<String>,
-
-    #[serde(rename = "title_bar.background")]
-    pub title_bar_background: Option<String>,
-
-    #[serde(rename = "title_bar.inactive_background")]
-    pub title_bar_inactive_background: Option<String>,
-
-    #[serde(rename = "toolbar.background")]
-    pub toolbar_background: Option<String>,
-
-    #[serde(rename = "tab_bar.background")]
-    pub tab_bar_background: Option<String>,
-
-    #[serde(rename = "tab.inactive_background")]
-    pub tab_inactive_background: Option<String>,
-
-    #[serde(rename = "tab.active_background")]
-    pub tab_active_background: Option<String>,
-
-    #[serde(rename = "search.match_background")]
-    pub search_match_background: Option<String>,
-
-    #[serde(rename = "panel.background")]
-    pub panel_background: Option<String>,
-
-    #[serde(rename = "panel.focused_border")]
-    pub panel_focused_border: Option<String>,
-
-    #[serde(rename = "panel.indent_guide")]
-    pub panel_indent_guide: Option<String>,
-
-    #[serde(rename = "panel.indent_guide_hover")]
-    pub panel_indent_guide_hover: Option<String>,
-
-    #[serde(rename = "panel.indent_guide_active")]
-    pub panel_indent_guide_active: Option<String>,
-
-    #[serde(rename = "panel.overlay_background")]
-    pub panel_overlay_background: Option<String>,
-
-    #[serde(rename = "panel.overlay_hover")]
-    pub panel_overlay_hover: Option<String>,
-
-    #[serde(rename = "pane.focused_border")]
-    pub pane_focused_border: Option<String>,
-
-    #[serde(rename = "pane_group.border")]
-    pub pane_group_border: Option<String>,
-
-    /// The deprecated version of `scrollbar.thumb.background`.
-    ///
-    /// Don't use this field.
-    #[serde(rename = "scrollbar_thumb.background", skip_serializing)]
-    #[schemars(skip)]
-    pub deprecated_scrollbar_thumb_background: Option<String>,
-
-    /// The color of the scrollbar thumb.
-    #[serde(rename = "scrollbar.thumb.background")]
-    pub scrollbar_thumb_background: Option<String>,
-
-    /// The color of the scrollbar thumb when hovered over.
-    #[serde(rename = "scrollbar.thumb.hover_background")]
-    pub scrollbar_thumb_hover_background: Option<String>,
-
-    /// The color of the scrollbar thumb whilst being actively dragged.
-    #[serde(rename = "scrollbar.thumb.active_background")]
-    pub scrollbar_thumb_active_background: Option<String>,
-
-    /// The border color of the scrollbar thumb.
-    #[serde(rename = "scrollbar.thumb.border")]
-    pub scrollbar_thumb_border: Option<String>,
-
-    /// The background color of the scrollbar track.
-    #[serde(rename = "scrollbar.track.background")]
-    pub scrollbar_track_background: Option<String>,
-
-    /// The border color of the scrollbar track.
-    #[serde(rename = "scrollbar.track.border")]
-    pub scrollbar_track_border: Option<String>,
-
-    /// The color of the minimap thumb.
-    #[serde(rename = "minimap.thumb.background")]
-    pub minimap_thumb_background: Option<String>,
-
-    /// The color of the minimap thumb when hovered over.
-    #[serde(rename = "minimap.thumb.hover_background")]
-    pub minimap_thumb_hover_background: Option<String>,
-
-    /// The color of the minimap thumb whilst being actively dragged.
-    #[serde(rename = "minimap.thumb.active_background")]
-    pub minimap_thumb_active_background: Option<String>,
-
-    /// The border color of the minimap thumb.
-    #[serde(rename = "minimap.thumb.border")]
-    pub minimap_thumb_border: Option<String>,
-
-    #[serde(rename = "editor.foreground")]
-    pub editor_foreground: Option<String>,
-
-    #[serde(rename = "editor.background")]
-    pub editor_background: Option<String>,
-
-    #[serde(rename = "editor.gutter.background")]
-    pub editor_gutter_background: Option<String>,
-
-    #[serde(rename = "editor.subheader.background")]
-    pub editor_subheader_background: Option<String>,
-
-    #[serde(rename = "editor.active_line.background")]
-    pub editor_active_line_background: Option<String>,
-
-    #[serde(rename = "editor.highlighted_line.background")]
-    pub editor_highlighted_line_background: Option<String>,
-
-    /// Background of active line of debugger
-    #[serde(rename = "editor.debugger_active_line.background")]
-    pub editor_debugger_active_line_background: Option<String>,
-
-    /// Text Color. Used for the text of the line number in the editor gutter.
-    #[serde(rename = "editor.line_number")]
-    pub editor_line_number: Option<String>,
-
-    /// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted.
-    #[serde(rename = "editor.active_line_number")]
-    pub editor_active_line_number: Option<String>,
-
-    /// Text Color. Used for the text of the line number in the editor gutter when the line is hovered over.
-    #[serde(rename = "editor.hover_line_number")]
-    pub editor_hover_line_number: Option<String>,
-
-    /// Text Color. Used to mark invisible characters in the editor.
-    ///
-    /// Example: spaces, tabs, carriage returns, etc.
-    #[serde(rename = "editor.invisible")]
-    pub editor_invisible: Option<String>,
-
-    #[serde(rename = "editor.wrap_guide")]
-    pub editor_wrap_guide: Option<String>,
-
-    #[serde(rename = "editor.active_wrap_guide")]
-    pub editor_active_wrap_guide: Option<String>,
-
-    #[serde(rename = "editor.indent_guide")]
-    pub editor_indent_guide: Option<String>,
-
-    #[serde(rename = "editor.indent_guide_active")]
-    pub editor_indent_guide_active: Option<String>,
-
-    /// Read-access of a symbol, like reading a variable.
-    ///
-    /// A document highlight is a range inside a text document which deserves
-    /// special attention. Usually a document highlight is visualized by changing
-    /// the background color of its range.
-    #[serde(rename = "editor.document_highlight.read_background")]
-    pub editor_document_highlight_read_background: Option<String>,
-
-    /// Read-access of a symbol, like reading a variable.
-    ///
-    /// A document highlight is a range inside a text document which deserves
-    /// special attention. Usually a document highlight is visualized by changing
-    /// the background color of its range.
-    #[serde(rename = "editor.document_highlight.write_background")]
-    pub editor_document_highlight_write_background: Option<String>,
-
-    /// Highlighted brackets background color.
-    ///
-    /// Matching brackets in the cursor scope are highlighted with this background color.
-    #[serde(rename = "editor.document_highlight.bracket_background")]
-    pub editor_document_highlight_bracket_background: Option<String>,
-
-    /// Terminal background color.
-    #[serde(rename = "terminal.background")]
-    pub terminal_background: Option<String>,
-
-    /// Terminal foreground color.
-    #[serde(rename = "terminal.foreground")]
-    pub terminal_foreground: Option<String>,
-
-    /// Terminal ANSI background color.
-    #[serde(rename = "terminal.ansi.background")]
-    pub terminal_ansi_background: Option<String>,
-
-    /// Bright terminal foreground color.
-    #[serde(rename = "terminal.bright_foreground")]
-    pub terminal_bright_foreground: Option<String>,
-
-    /// Dim terminal foreground color.
-    #[serde(rename = "terminal.dim_foreground")]
-    pub terminal_dim_foreground: Option<String>,
-
-    /// Black ANSI terminal color.
-    #[serde(rename = "terminal.ansi.black")]
-    pub terminal_ansi_black: Option<String>,
-
-    /// Bright black ANSI terminal color.
-    #[serde(rename = "terminal.ansi.bright_black")]
-    pub terminal_ansi_bright_black: Option<String>,
-
-    /// Dim black ANSI terminal color.
-    #[serde(rename = "terminal.ansi.dim_black")]
-    pub terminal_ansi_dim_black: Option<String>,
-
-    /// Red ANSI terminal color.
-    #[serde(rename = "terminal.ansi.red")]
-    pub terminal_ansi_red: Option<String>,
-
-    /// Bright red ANSI terminal color.
-    #[serde(rename = "terminal.ansi.bright_red")]
-    pub terminal_ansi_bright_red: Option<String>,
-
-    /// Dim red ANSI terminal color.
-    #[serde(rename = "terminal.ansi.dim_red")]
-    pub terminal_ansi_dim_red: Option<String>,
-
-    /// Green ANSI terminal color.
-    #[serde(rename = "terminal.ansi.green")]
-    pub terminal_ansi_green: Option<String>,
-
-    /// Bright green ANSI terminal color.
-    #[serde(rename = "terminal.ansi.bright_green")]
-    pub terminal_ansi_bright_green: Option<String>,
-
-    /// Dim green ANSI terminal color.
-    #[serde(rename = "terminal.ansi.dim_green")]
-    pub terminal_ansi_dim_green: Option<String>,
-
-    /// Yellow ANSI terminal color.
-    #[serde(rename = "terminal.ansi.yellow")]
-    pub terminal_ansi_yellow: Option<String>,
-
-    /// Bright yellow ANSI terminal color.
-    #[serde(rename = "terminal.ansi.bright_yellow")]
-    pub terminal_ansi_bright_yellow: Option<String>,
-
-    /// Dim yellow ANSI terminal color.
-    #[serde(rename = "terminal.ansi.dim_yellow")]
-    pub terminal_ansi_dim_yellow: Option<String>,
-
-    /// Blue ANSI terminal color.
-    #[serde(rename = "terminal.ansi.blue")]
-    pub terminal_ansi_blue: Option<String>,
-
-    /// Bright blue ANSI terminal color.
-    #[serde(rename = "terminal.ansi.bright_blue")]
-    pub terminal_ansi_bright_blue: Option<String>,
-
-    /// Dim blue ANSI terminal color.
-    #[serde(rename = "terminal.ansi.dim_blue")]
-    pub terminal_ansi_dim_blue: Option<String>,
-
-    /// Magenta ANSI terminal color.
-    #[serde(rename = "terminal.ansi.magenta")]
-    pub terminal_ansi_magenta: Option<String>,
-
-    /// Bright magenta ANSI terminal color.
-    #[serde(rename = "terminal.ansi.bright_magenta")]
-    pub terminal_ansi_bright_magenta: Option<String>,
-
-    /// Dim magenta ANSI terminal color.
-    #[serde(rename = "terminal.ansi.dim_magenta")]
-    pub terminal_ansi_dim_magenta: Option<String>,
-
-    /// Cyan ANSI terminal color.
-    #[serde(rename = "terminal.ansi.cyan")]
-    pub terminal_ansi_cyan: Option<String>,
-
-    /// Bright cyan ANSI terminal color.
-    #[serde(rename = "terminal.ansi.bright_cyan")]
-    pub terminal_ansi_bright_cyan: Option<String>,
-
-    /// Dim cyan ANSI terminal color.
-    #[serde(rename = "terminal.ansi.dim_cyan")]
-    pub terminal_ansi_dim_cyan: Option<String>,
-
-    /// White ANSI terminal color.
-    #[serde(rename = "terminal.ansi.white")]
-    pub terminal_ansi_white: Option<String>,
-
-    /// Bright white ANSI terminal color.
-    #[serde(rename = "terminal.ansi.bright_white")]
-    pub terminal_ansi_bright_white: Option<String>,
-
-    /// Dim white ANSI terminal color.
-    #[serde(rename = "terminal.ansi.dim_white")]
-    pub terminal_ansi_dim_white: Option<String>,
-
-    #[serde(rename = "link_text.hover")]
-    pub link_text_hover: Option<String>,
-
-    /// Added version control color.
-    #[serde(rename = "version_control.added")]
-    pub version_control_added: Option<String>,
-
-    /// Deleted version control color.
-    #[serde(rename = "version_control.deleted")]
-    pub version_control_deleted: Option<String>,
-
-    /// Modified version control color.
-    #[serde(rename = "version_control.modified")]
-    pub version_control_modified: Option<String>,
-
-    /// Renamed version control color.
-    #[serde(rename = "version_control.renamed")]
-    pub version_control_renamed: Option<String>,
-
-    /// Conflict version control color.
-    #[serde(rename = "version_control.conflict")]
-    pub version_control_conflict: Option<String>,
-
-    /// Ignored version control color.
-    #[serde(rename = "version_control.ignored")]
-    pub version_control_ignored: Option<String>,
-
-    /// Background color for row highlights of "ours" regions in merge conflicts.
-    #[serde(rename = "version_control.conflict_marker.ours")]
-    pub version_control_conflict_marker_ours: Option<String>,
-
-    /// Background color for row highlights of "theirs" regions in merge conflicts.
-    #[serde(rename = "version_control.conflict_marker.theirs")]
-    pub version_control_conflict_marker_theirs: Option<String>,
-
-    /// Deprecated in favor of `version_control_conflict_marker_ours`.
-    #[deprecated]
-    pub version_control_conflict_ours_background: Option<String>,
-
-    /// Deprecated in favor of `version_control_conflict_marker_theirs`.
-    #[deprecated]
-    pub version_control_conflict_theirs_background: Option<String>,
+pub fn status_colors_refinement(colors: &settings::StatusColorsContent) -> StatusColorsRefinement {
+    StatusColorsRefinement {
+        conflict: colors
+            .conflict
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        conflict_background: colors
+            .conflict_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        conflict_border: colors
+            .conflict_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        created: colors
+            .created
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        created_background: colors
+            .created_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        created_border: colors
+            .created_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        deleted: colors
+            .deleted
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        deleted_background: colors
+            .deleted_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        deleted_border: colors
+            .deleted_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        error: colors
+            .error
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        error_background: colors
+            .error_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        error_border: colors
+            .error_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hidden: colors
+            .hidden
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hidden_background: colors
+            .hidden_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hidden_border: colors
+            .hidden_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hint: colors
+            .hint
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hint_background: colors
+            .hint_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        hint_border: colors
+            .hint_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ignored: colors
+            .ignored
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ignored_background: colors
+            .ignored_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ignored_border: colors
+            .ignored_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        info: colors
+            .info
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        info_background: colors
+            .info_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        info_border: colors
+            .info_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        modified: colors
+            .modified
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        modified_background: colors
+            .modified_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        modified_border: colors
+            .modified_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        predictive: colors
+            .predictive
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        predictive_background: colors
+            .predictive_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        predictive_border: colors
+            .predictive_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        renamed: colors
+            .renamed
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        renamed_background: colors
+            .renamed_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        renamed_border: colors
+            .renamed_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        success: colors
+            .success
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        success_background: colors
+            .success_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        success_border: colors
+            .success_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        unreachable: colors
+            .unreachable
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        unreachable_background: colors
+            .unreachable_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        unreachable_border: colors
+            .unreachable_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        warning: colors
+            .warning
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        warning_background: colors
+            .warning_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        warning_border: colors
+            .warning_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+    }
 }
 
-impl ThemeColorsContent {
-    /// Returns a [`ThemeColorsRefinement`] based on the colors in the [`ThemeColorsContent`].
-    pub fn theme_colors_refinement(
-        &self,
-        status_colors: &StatusColorsRefinement,
-    ) -> ThemeColorsRefinement {
-        let border = self
-            .border
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        let editor_document_highlight_read_background = self
-            .editor_document_highlight_read_background
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        let scrollbar_thumb_background = self
-            .scrollbar_thumb_background
+pub fn theme_colors_refinement(
+    this: &settings::ThemeColorsContent,
+    status_colors: &StatusColorsRefinement,
+) -> ThemeColorsRefinement {
+    let border = this
+        .border
+        .as_ref()
+        .and_then(|color| try_parse_color(color).ok());
+    let editor_document_highlight_read_background = this
+        .editor_document_highlight_read_background
+        .as_ref()
+        .and_then(|color| try_parse_color(color).ok());
+    let scrollbar_thumb_background = this
+        .scrollbar_thumb_background
+        .as_ref()
+        .and_then(|color| try_parse_color(color).ok())
+        .or_else(|| {
+            this.deprecated_scrollbar_thumb_background
+                .as_ref()
+                .and_then(|color| try_parse_color(color).ok())
+        });
+    let scrollbar_thumb_hover_background = this
+        .scrollbar_thumb_hover_background
+        .as_ref()
+        .and_then(|color| try_parse_color(color).ok());
+    let scrollbar_thumb_active_background = this
+        .scrollbar_thumb_active_background
+        .as_ref()
+        .and_then(|color| try_parse_color(color).ok())
+        .or(scrollbar_thumb_background);
+    let scrollbar_thumb_border = this
+        .scrollbar_thumb_border
+        .as_ref()
+        .and_then(|color| try_parse_color(color).ok());
+    let element_hover = this
+        .element_hover
+        .as_ref()
+        .and_then(|color| try_parse_color(color).ok());
+    let panel_background = this
+        .panel_background
+        .as_ref()
+        .and_then(|color| try_parse_color(color).ok());
+    ThemeColorsRefinement {
+        border,
+        border_variant: this
+            .border_variant
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        border_focused: this
+            .border_focused
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        border_selected: this
+            .border_selected
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        border_transparent: this
+            .border_transparent
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        border_disabled: this
+            .border_disabled
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        elevated_surface_background: this
+            .elevated_surface_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        surface_background: this
+            .surface_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        background: this
+            .background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        element_background: this
+            .element_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        element_hover,
+        element_active: this
+            .element_active
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        element_selected: this
+            .element_selected
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        element_disabled: this
+            .element_disabled
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        element_selection_background: this
+            .element_selection_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        drop_target_background: this
+            .drop_target_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        drop_target_border: this
+            .drop_target_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ghost_element_background: this
+            .ghost_element_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ghost_element_hover: this
+            .ghost_element_hover
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ghost_element_active: this
+            .ghost_element_active
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ghost_element_selected: this
+            .ghost_element_selected
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        ghost_element_disabled: this
+            .ghost_element_disabled
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        text: this
+            .text
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        text_muted: this
+            .text_muted
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        text_placeholder: this
+            .text_placeholder
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        text_disabled: this
+            .text_disabled
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        text_accent: this
+            .text_accent
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        icon: this
+            .icon
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        icon_muted: this
+            .icon_muted
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        icon_disabled: this
+            .icon_disabled
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        icon_placeholder: this
+            .icon_placeholder
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        icon_accent: this
+            .icon_accent
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        debugger_accent: this
+            .debugger_accent
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        status_bar_background: this
+            .status_bar_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        title_bar_background: this
+            .title_bar_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        title_bar_inactive_background: this
+            .title_bar_inactive_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        toolbar_background: this
+            .toolbar_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        tab_bar_background: this
+            .tab_bar_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        tab_inactive_background: this
+            .tab_inactive_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        tab_active_background: this
+            .tab_active_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        search_match_background: this
+            .search_match_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        panel_background,
+        panel_focused_border: this
+            .panel_focused_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        panel_indent_guide: this
+            .panel_indent_guide
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        panel_indent_guide_hover: this
+            .panel_indent_guide_hover
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        panel_indent_guide_active: this
+            .panel_indent_guide_active
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        panel_overlay_background: this
+            .panel_overlay_background
             .as_ref()
             .and_then(|color| try_parse_color(color).ok())
-            .or_else(|| {
-                self.deprecated_scrollbar_thumb_background
-                    .as_ref()
-                    .and_then(|color| try_parse_color(color).ok())
-            });
-        let scrollbar_thumb_hover_background = self
-            .scrollbar_thumb_hover_background
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        let scrollbar_thumb_active_background = self
-            .scrollbar_thumb_active_background
+            .or(panel_background.map(ensure_opaque)),
+        panel_overlay_hover: this
+            .panel_overlay_hover
             .as_ref()
             .and_then(|color| try_parse_color(color).ok())
-            .or(scrollbar_thumb_background);
-        let scrollbar_thumb_border = self
-            .scrollbar_thumb_border
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        let element_hover = self
-            .element_hover
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        let panel_background = self
-            .panel_background
-            .as_ref()
-            .and_then(|color| try_parse_color(color).ok());
-        ThemeColorsRefinement {
-            border,
-            border_variant: self
-                .border_variant
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            border_focused: self
-                .border_focused
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            border_selected: self
-                .border_selected
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            border_transparent: self
-                .border_transparent
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            border_disabled: self
-                .border_disabled
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            elevated_surface_background: self
-                .elevated_surface_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            surface_background: self
-                .surface_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            background: self
-                .background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            element_background: self
-                .element_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            element_hover,
-            element_active: self
-                .element_active
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            element_selected: self
-                .element_selected
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            element_disabled: self
-                .element_disabled
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            element_selection_background: self
-                .element_selection_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            drop_target_background: self
-                .drop_target_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            drop_target_border: self
-                .drop_target_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ghost_element_background: self
-                .ghost_element_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ghost_element_hover: self
-                .ghost_element_hover
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ghost_element_active: self
-                .ghost_element_active
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ghost_element_selected: self
-                .ghost_element_selected
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ghost_element_disabled: self
-                .ghost_element_disabled
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            text: self
-                .text
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            text_muted: self
-                .text_muted
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            text_placeholder: self
-                .text_placeholder
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            text_disabled: self
-                .text_disabled
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            text_accent: self
-                .text_accent
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            icon: self
-                .icon
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            icon_muted: self
-                .icon_muted
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            icon_disabled: self
-                .icon_disabled
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            icon_placeholder: self
-                .icon_placeholder
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            icon_accent: self
-                .icon_accent
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            debugger_accent: self
-                .debugger_accent
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            status_bar_background: self
-                .status_bar_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            title_bar_background: self
-                .title_bar_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            title_bar_inactive_background: self
-                .title_bar_inactive_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            toolbar_background: self
-                .toolbar_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            tab_bar_background: self
-                .tab_bar_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            tab_inactive_background: self
-                .tab_inactive_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            tab_active_background: self
-                .tab_active_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            search_match_background: self
-                .search_match_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            panel_background,
-            panel_focused_border: self
-                .panel_focused_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            panel_indent_guide: self
-                .panel_indent_guide
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            panel_indent_guide_hover: self
-                .panel_indent_guide_hover
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            panel_indent_guide_active: self
-                .panel_indent_guide_active
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            panel_overlay_background: self
-                .panel_overlay_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(panel_background.map(ensure_opaque)),
-            panel_overlay_hover: self
-                .panel_overlay_hover
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(panel_background
-                    .zip(element_hover)
-                    .map(|(panel_bg, hover_bg)| panel_bg.blend(hover_bg))
-                    .map(ensure_opaque)),
-            pane_focused_border: self
-                .pane_focused_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            pane_group_border: self
-                .pane_group_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(border),
-            scrollbar_thumb_background,
-            scrollbar_thumb_hover_background,
-            scrollbar_thumb_active_background,
-            scrollbar_thumb_border,
-            scrollbar_track_background: self
-                .scrollbar_track_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            scrollbar_track_border: self
-                .scrollbar_track_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            minimap_thumb_background: self
-                .minimap_thumb_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(scrollbar_thumb_background.map(ensure_non_opaque)),
-            minimap_thumb_hover_background: self
-                .minimap_thumb_hover_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(scrollbar_thumb_hover_background.map(ensure_non_opaque)),
-            minimap_thumb_active_background: self
-                .minimap_thumb_active_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(scrollbar_thumb_active_background.map(ensure_non_opaque)),
-            minimap_thumb_border: self
-                .minimap_thumb_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                .or(scrollbar_thumb_border),
-            editor_foreground: self
-                .editor_foreground
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_background: self
-                .editor_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_gutter_background: self
-                .editor_gutter_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_subheader_background: self
-                .editor_subheader_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_active_line_background: self
-                .editor_active_line_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_highlighted_line_background: self
-                .editor_highlighted_line_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_debugger_active_line_background: self
-                .editor_debugger_active_line_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_line_number: self
-                .editor_line_number
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_hover_line_number: self
-                .editor_hover_line_number
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_active_line_number: self
-                .editor_active_line_number
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_invisible: self
-                .editor_invisible
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_wrap_guide: self
-                .editor_wrap_guide
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_active_wrap_guide: self
-                .editor_active_wrap_guide
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_indent_guide: self
-                .editor_indent_guide
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_indent_guide_active: self
-                .editor_indent_guide_active
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_document_highlight_read_background,
-            editor_document_highlight_write_background: self
-                .editor_document_highlight_write_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            editor_document_highlight_bracket_background: self
-                .editor_document_highlight_bracket_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `editor.document_highlight.read_background`, for backwards compatibility.
-                .or(editor_document_highlight_read_background),
-            terminal_background: self
-                .terminal_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_background: self
-                .terminal_ansi_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_foreground: self
-                .terminal_foreground
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_bright_foreground: self
-                .terminal_bright_foreground
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_dim_foreground: self
-                .terminal_dim_foreground
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_black: self
-                .terminal_ansi_black
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_black: self
-                .terminal_ansi_bright_black
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_black: self
-                .terminal_ansi_dim_black
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_red: self
-                .terminal_ansi_red
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_red: self
-                .terminal_ansi_bright_red
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_red: self
-                .terminal_ansi_dim_red
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_green: self
-                .terminal_ansi_green
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_green: self
-                .terminal_ansi_bright_green
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_green: self
-                .terminal_ansi_dim_green
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_yellow: self
-                .terminal_ansi_yellow
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_yellow: self
-                .terminal_ansi_bright_yellow
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_yellow: self
-                .terminal_ansi_dim_yellow
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_blue: self
-                .terminal_ansi_blue
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_blue: self
-                .terminal_ansi_bright_blue
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_blue: self
-                .terminal_ansi_dim_blue
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_magenta: self
-                .terminal_ansi_magenta
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_magenta: self
-                .terminal_ansi_bright_magenta
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_magenta: self
-                .terminal_ansi_dim_magenta
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_cyan: self
-                .terminal_ansi_cyan
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_cyan: self
-                .terminal_ansi_bright_cyan
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_cyan: self
-                .terminal_ansi_dim_cyan
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_white: self
-                .terminal_ansi_white
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_bright_white: self
-                .terminal_ansi_bright_white
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            terminal_ansi_dim_white: self
-                .terminal_ansi_dim_white
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            link_text_hover: self
-                .link_text_hover
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            version_control_added: self
-                .version_control_added
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `created`, for backwards compatibility.
-                .or(status_colors.created),
-            version_control_deleted: self
-                .version_control_deleted
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `deleted`, for backwards compatibility.
-                .or(status_colors.deleted),
-            version_control_modified: self
-                .version_control_modified
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `modified`, for backwards compatibility.
-                .or(status_colors.modified),
-            version_control_renamed: self
-                .version_control_renamed
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `modified`, for backwards compatibility.
-                .or(status_colors.modified),
-            version_control_conflict: self
-                .version_control_conflict
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `ignored`, for backwards compatibility.
-                .or(status_colors.ignored),
-            version_control_ignored: self
-                .version_control_ignored
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok())
-                // Fall back to `conflict`, for backwards compatibility.
-                .or(status_colors.ignored),
-            #[allow(deprecated)]
-            version_control_conflict_marker_ours: self
-                .version_control_conflict_marker_ours
-                .as_ref()
-                .or(self.version_control_conflict_ours_background.as_ref())
-                .and_then(|color| try_parse_color(color).ok()),
-            #[allow(deprecated)]
-            version_control_conflict_marker_theirs: self
-                .version_control_conflict_marker_theirs
-                .as_ref()
-                .or(self.version_control_conflict_theirs_background.as_ref())
-                .and_then(|color| try_parse_color(color).ok()),
-        }
-    }
-}
-
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
-#[serde(default)]
-pub struct StatusColorsContent {
-    /// Indicates some kind of conflict, like a file changed on disk while it was open, or
-    /// merge conflicts in a Git repository.
-    #[serde(rename = "conflict")]
-    pub conflict: Option<String>,
-
-    #[serde(rename = "conflict.background")]
-    pub conflict_background: Option<String>,
-
-    #[serde(rename = "conflict.border")]
-    pub conflict_border: Option<String>,
-
-    /// Indicates something new, like a new file added to a Git repository.
-    #[serde(rename = "created")]
-    pub created: Option<String>,
-
-    #[serde(rename = "created.background")]
-    pub created_background: Option<String>,
-
-    #[serde(rename = "created.border")]
-    pub created_border: Option<String>,
-
-    /// Indicates that something no longer exists, like a deleted file.
-    #[serde(rename = "deleted")]
-    pub deleted: Option<String>,
-
-    #[serde(rename = "deleted.background")]
-    pub deleted_background: Option<String>,
-
-    #[serde(rename = "deleted.border")]
-    pub deleted_border: Option<String>,
-
-    /// Indicates a system error, a failed operation or a diagnostic error.
-    #[serde(rename = "error")]
-    pub error: Option<String>,
-
-    #[serde(rename = "error.background")]
-    pub error_background: Option<String>,
-
-    #[serde(rename = "error.border")]
-    pub error_border: Option<String>,
-
-    /// Represents a hidden status, such as a file being hidden in a file tree.
-    #[serde(rename = "hidden")]
-    pub hidden: Option<String>,
-
-    #[serde(rename = "hidden.background")]
-    pub hidden_background: Option<String>,
-
-    #[serde(rename = "hidden.border")]
-    pub hidden_border: Option<String>,
-
-    /// Indicates a hint or some kind of additional information.
-    #[serde(rename = "hint")]
-    pub hint: Option<String>,
-
-    #[serde(rename = "hint.background")]
-    pub hint_background: Option<String>,
-
-    #[serde(rename = "hint.border")]
-    pub hint_border: Option<String>,
-
-    /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git.
-    #[serde(rename = "ignored")]
-    pub ignored: Option<String>,
-
-    #[serde(rename = "ignored.background")]
-    pub ignored_background: Option<String>,
-
-    #[serde(rename = "ignored.border")]
-    pub ignored_border: Option<String>,
-
-    /// Represents informational status updates or messages.
-    #[serde(rename = "info")]
-    pub info: Option<String>,
-
-    #[serde(rename = "info.background")]
-    pub info_background: Option<String>,
-
-    #[serde(rename = "info.border")]
-    pub info_border: Option<String>,
-
-    /// Indicates a changed or altered status, like a file that has been edited.
-    #[serde(rename = "modified")]
-    pub modified: Option<String>,
-
-    #[serde(rename = "modified.background")]
-    pub modified_background: Option<String>,
-
-    #[serde(rename = "modified.border")]
-    pub modified_border: Option<String>,
-
-    /// Indicates something that is predicted, like automatic code completion, or generated code.
-    #[serde(rename = "predictive")]
-    pub predictive: Option<String>,
-
-    #[serde(rename = "predictive.background")]
-    pub predictive_background: Option<String>,
-
-    #[serde(rename = "predictive.border")]
-    pub predictive_border: Option<String>,
-
-    /// Represents a renamed status, such as a file that has been renamed.
-    #[serde(rename = "renamed")]
-    pub renamed: Option<String>,
-
-    #[serde(rename = "renamed.background")]
-    pub renamed_background: Option<String>,
-
-    #[serde(rename = "renamed.border")]
-    pub renamed_border: Option<String>,
-
-    /// Indicates a successful operation or task completion.
-    #[serde(rename = "success")]
-    pub success: Option<String>,
-
-    #[serde(rename = "success.background")]
-    pub success_background: Option<String>,
-
-    #[serde(rename = "success.border")]
-    pub success_border: Option<String>,
-
-    /// Indicates some kind of unreachable status, like a block of code that can never be reached.
-    #[serde(rename = "unreachable")]
-    pub unreachable: Option<String>,
-
-    #[serde(rename = "unreachable.background")]
-    pub unreachable_background: Option<String>,
-
-    #[serde(rename = "unreachable.border")]
-    pub unreachable_border: Option<String>,
-
-    /// Represents a warning status, like an operation that is about to fail.
-    #[serde(rename = "warning")]
-    pub warning: Option<String>,
-
-    #[serde(rename = "warning.background")]
-    pub warning_background: Option<String>,
-
-    #[serde(rename = "warning.border")]
-    pub warning_border: Option<String>,
-}
-
-impl StatusColorsContent {
-    /// Returns a [`StatusColorsRefinement`] based on the colors in the [`StatusColorsContent`].
-    pub fn status_colors_refinement(&self) -> StatusColorsRefinement {
-        StatusColorsRefinement {
-            conflict: self
-                .conflict
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            conflict_background: self
-                .conflict_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            conflict_border: self
-                .conflict_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            created: self
-                .created
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            created_background: self
-                .created_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            created_border: self
-                .created_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            deleted: self
-                .deleted
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            deleted_background: self
-                .deleted_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            deleted_border: self
-                .deleted_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            error: self
-                .error
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            error_background: self
-                .error_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            error_border: self
-                .error_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hidden: self
-                .hidden
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hidden_background: self
-                .hidden_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hidden_border: self
-                .hidden_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hint: self
-                .hint
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hint_background: self
-                .hint_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            hint_border: self
-                .hint_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ignored: self
-                .ignored
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ignored_background: self
-                .ignored_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            ignored_border: self
-                .ignored_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            info: self
-                .info
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            info_background: self
-                .info_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            info_border: self
-                .info_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            modified: self
-                .modified
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            modified_background: self
-                .modified_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            modified_border: self
-                .modified_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            predictive: self
-                .predictive
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            predictive_background: self
-                .predictive_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            predictive_border: self
-                .predictive_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            renamed: self
-                .renamed
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            renamed_background: self
-                .renamed_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            renamed_border: self
-                .renamed_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            success: self
-                .success
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            success_background: self
-                .success_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            success_border: self
-                .success_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            unreachable: self
-                .unreachable
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            unreachable_background: self
-                .unreachable_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            unreachable_border: self
-                .unreachable_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            warning: self
-                .warning
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            warning_background: self
-                .warning_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-            warning_border: self
-                .warning_border
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
-        }
-    }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
-pub struct AccentContent(pub Option<String>);
-
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
-pub struct PlayerColorContent {
-    pub cursor: Option<String>,
-    pub background: Option<String>,
-    pub selection: Option<String>,
-}
-
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq)]
-#[serde(rename_all = "snake_case")]
-pub enum FontStyleContent {
-    Normal,
-    Italic,
-    Oblique,
-}
-
-impl From<FontStyleContent> for FontStyle {
-    fn from(value: FontStyleContent) -> Self {
-        match value {
-            FontStyleContent::Normal => FontStyle::Normal,
-            FontStyleContent::Italic => FontStyle::Italic,
-            FontStyleContent::Oblique => FontStyle::Oblique,
-        }
-    }
-}
-
-#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq)]
-#[repr(u16)]
-pub enum FontWeightContent {
-    Thin = 100,
-    ExtraLight = 200,
-    Light = 300,
-    Normal = 400,
-    Medium = 500,
-    Semibold = 600,
-    Bold = 700,
-    ExtraBold = 800,
-    Black = 900,
-}
-
-impl From<FontWeightContent> for FontWeight {
-    fn from(value: FontWeightContent) -> Self {
-        match value {
-            FontWeightContent::Thin => FontWeight::THIN,
-            FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT,
-            FontWeightContent::Light => FontWeight::LIGHT,
-            FontWeightContent::Normal => FontWeight::NORMAL,
-            FontWeightContent::Medium => FontWeight::MEDIUM,
-            FontWeightContent::Semibold => FontWeight::SEMIBOLD,
-            FontWeightContent::Bold => FontWeight::BOLD,
-            FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD,
-            FontWeightContent::Black => FontWeight::BLACK,
-        }
+            .or(panel_background
+                .zip(element_hover)
+                .map(|(panel_bg, hover_bg)| panel_bg.blend(hover_bg))
+                .map(ensure_opaque)),
+        pane_focused_border: this
+            .pane_focused_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        pane_group_border: this
+            .pane_group_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            .or(border),
+        scrollbar_thumb_background,
+        scrollbar_thumb_hover_background,
+        scrollbar_thumb_active_background,
+        scrollbar_thumb_border,
+        scrollbar_track_background: this
+            .scrollbar_track_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        scrollbar_track_border: this
+            .scrollbar_track_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        minimap_thumb_background: this
+            .minimap_thumb_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            .or(scrollbar_thumb_background.map(ensure_non_opaque)),
+        minimap_thumb_hover_background: this
+            .minimap_thumb_hover_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            .or(scrollbar_thumb_hover_background.map(ensure_non_opaque)),
+        minimap_thumb_active_background: this
+            .minimap_thumb_active_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            .or(scrollbar_thumb_active_background.map(ensure_non_opaque)),
+        minimap_thumb_border: this
+            .minimap_thumb_border
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            .or(scrollbar_thumb_border),
+        editor_foreground: this
+            .editor_foreground
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_background: this
+            .editor_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_gutter_background: this
+            .editor_gutter_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_subheader_background: this
+            .editor_subheader_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_active_line_background: this
+            .editor_active_line_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_highlighted_line_background: this
+            .editor_highlighted_line_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_debugger_active_line_background: this
+            .editor_debugger_active_line_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_line_number: this
+            .editor_line_number
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_hover_line_number: this
+            .editor_hover_line_number
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_active_line_number: this
+            .editor_active_line_number
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_invisible: this
+            .editor_invisible
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_wrap_guide: this
+            .editor_wrap_guide
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_active_wrap_guide: this
+            .editor_active_wrap_guide
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_indent_guide: this
+            .editor_indent_guide
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_indent_guide_active: this
+            .editor_indent_guide_active
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_document_highlight_read_background,
+        editor_document_highlight_write_background: this
+            .editor_document_highlight_write_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        editor_document_highlight_bracket_background: this
+            .editor_document_highlight_bracket_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            // Fall back to `editor.document_highlight.read_background`, for backwards compatibility.
+            .or(editor_document_highlight_read_background),
+        terminal_background: this
+            .terminal_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_background: this
+            .terminal_ansi_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_foreground: this
+            .terminal_foreground
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_bright_foreground: this
+            .terminal_bright_foreground
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_dim_foreground: this
+            .terminal_dim_foreground
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_black: this
+            .terminal_ansi_black
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_bright_black: this
+            .terminal_ansi_bright_black
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_dim_black: this
+            .terminal_ansi_dim_black
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_red: this
+            .terminal_ansi_red
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_bright_red: this
+            .terminal_ansi_bright_red
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_dim_red: this
+            .terminal_ansi_dim_red
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_green: this
+            .terminal_ansi_green
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_bright_green: this
+            .terminal_ansi_bright_green
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_dim_green: this
+            .terminal_ansi_dim_green
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_yellow: this
+            .terminal_ansi_yellow
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_bright_yellow: this
+            .terminal_ansi_bright_yellow
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_dim_yellow: this
+            .terminal_ansi_dim_yellow
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_blue: this
+            .terminal_ansi_blue
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_bright_blue: this
+            .terminal_ansi_bright_blue
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_dim_blue: this
+            .terminal_ansi_dim_blue
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_magenta: this
+            .terminal_ansi_magenta
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_bright_magenta: this
+            .terminal_ansi_bright_magenta
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_dim_magenta: this
+            .terminal_ansi_dim_magenta
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_cyan: this
+            .terminal_ansi_cyan
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_bright_cyan: this
+            .terminal_ansi_bright_cyan
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_dim_cyan: this
+            .terminal_ansi_dim_cyan
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_white: this
+            .terminal_ansi_white
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_bright_white: this
+            .terminal_ansi_bright_white
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        terminal_ansi_dim_white: this
+            .terminal_ansi_dim_white
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        link_text_hover: this
+            .link_text_hover
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok()),
+        version_control_added: this
+            .version_control_added
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            // Fall back to `created`, for backwards compatibility.
+            .or(status_colors.created),
+        version_control_deleted: this
+            .version_control_deleted
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            // Fall back to `deleted`, for backwards compatibility.
+            .or(status_colors.deleted),
+        version_control_modified: this
+            .version_control_modified
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            // Fall back to `modified`, for backwards compatibility.
+            .or(status_colors.modified),
+        version_control_renamed: this
+            .version_control_renamed
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            // Fall back to `modified`, for backwards compatibility.
+            .or(status_colors.modified),
+        version_control_conflict: this
+            .version_control_conflict
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            // Fall back to `ignored`, for backwards compatibility.
+            .or(status_colors.ignored),
+        version_control_ignored: this
+            .version_control_ignored
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok())
+            // Fall back to `conflict`, for backwards compatibility.
+            .or(status_colors.ignored),
+        #[allow(deprecated)]
+        version_control_conflict_marker_ours: this
+            .version_control_conflict_marker_ours
+            .as_ref()
+            .or(this.version_control_conflict_ours_background.as_ref())
+            .and_then(|color| try_parse_color(color).ok()),
+        #[allow(deprecated)]
+        version_control_conflict_marker_theirs: this
+            .version_control_conflict_marker_theirs
+            .as_ref()
+            .or(this.version_control_conflict_theirs_background.as_ref())
+            .and_then(|color| try_parse_color(color).ok()),
     }
 }
 
-#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
-#[serde(default)]
-pub struct HighlightStyleContent {
-    pub color: Option<String>,
-
-    #[serde(deserialize_with = "treat_error_as_none")]
-    pub background_color: Option<String>,
-
-    #[serde(deserialize_with = "treat_error_as_none")]
-    pub font_style: Option<FontStyleContent>,
-
-    #[serde(deserialize_with = "treat_error_as_none")]
-    pub font_weight: Option<FontWeightContent>,
-}
+pub(crate) fn try_parse_color(color: &str) -> anyhow::Result<Hsla> {
+    let rgba = gpui::Rgba::try_from(color)?;
+    let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a));
+    let hsla = palette::Hsla::from_color(rgba);
 
-impl HighlightStyleContent {
-    pub fn is_empty(&self) -> bool {
-        self.color.is_none()
-            && self.background_color.is_none()
-            && self.font_style.is_none()
-            && self.font_weight.is_none()
-    }
-}
+    let hsla = gpui::hsla(
+        hsla.hue.into_positive_degrees() / 360.,
+        hsla.saturation,
+        hsla.lightness,
+        hsla.alpha,
+    );
 
-fn treat_error_as_none<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
-where
-    T: Deserialize<'de>,
-    D: Deserializer<'de>,
-{
-    let value: Value = Deserialize::deserialize(deserializer)?;
-    Ok(T::deserialize(value).ok())
+    Ok(hsla)
 }

crates/theme/src/settings.rs πŸ”—

@@ -1,22 +1,23 @@
 use crate::fallback_themes::zed_default_dark;
 use crate::{
     Appearance, DEFAULT_ICON_THEME_NAME, IconTheme, IconThemeNotFoundError, SyntaxTheme, Theme,
-    ThemeNotFoundError, ThemeRegistry, ThemeStyleContent,
+    ThemeNotFoundError, ThemeRegistry, status_colors_refinement, syntax_overrides,
+    theme_colors_refinement,
 };
-use anyhow::Result;
 use collections::HashMap;
 use derive_more::{Deref, DerefMut};
 use gpui::{
-    App, Context, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels,
-    SharedString, Subscription, Window, px,
+    App, Context, Font, FontFallbacks, FontStyle, FontWeight, Global, Pixels, SharedString,
+    Subscription, Window, px,
 };
 use refineable::Refineable;
 use schemars::{JsonSchema, json_schema};
 use serde::{Deserialize, Serialize};
-use settings::{ParameterizedJsonSchema, Settings, SettingsKey, SettingsSources, SettingsUi};
+pub use settings::{FontFamilyName, IconThemeName, ThemeMode, ThemeName};
+use settings::{ParameterizedJsonSchema, Settings, SettingsContent};
 use std::sync::Arc;
-use util::ResultExt as _;
 use util::schemars::replace_subschema;
+use util::{MergeFrom, ResultExt as _};
 
 const MIN_FONT_SIZE: Pixels = px(6.0);
 const MAX_FONT_SIZE: Pixels = px(100.0);
@@ -86,6 +87,16 @@ impl From<UiDensity> for String {
     }
 }
 
+impl From<settings::UiDensity> for UiDensity {
+    fn from(val: settings::UiDensity) -> Self {
+        match val {
+            settings::UiDensity::Compact => Self::Compact,
+            settings::UiDensity::Default => Self::Default,
+            settings::UiDensity::Comfortable => Self::Comfortable,
+        }
+    }
+}
+
 /// Customizable settings for the UI and theme system.
 #[derive(Clone, PartialEq)]
 pub struct ThemeSettings {
@@ -119,9 +130,9 @@ pub struct ThemeSettings {
     /// Manual overrides for the active theme.
     ///
     /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078)
-    pub experimental_theme_overrides: Option<ThemeStyleContent>,
+    pub experimental_theme_overrides: Option<settings::ThemeStyleContent>,
     /// Manual overrides per theme
-    pub theme_overrides: HashMap<String, ThemeStyleContent>,
+    pub theme_overrides: HashMap<String, settings::ThemeStyleContent>,
     /// The current icon theme selection.
     pub icon_theme_selection: Option<IconThemeSelection>,
     /// The active icon theme.
@@ -259,6 +270,45 @@ pub struct AgentFontSize(Pixels);
 
 impl Global for AgentFontSize {}
 
+inventory::submit! {
+    ParameterizedJsonSchema {
+        add_and_get_ref: |generator, _params, cx| {
+            replace_subschema::<settings::ThemeName>(generator, || json_schema!({
+                "type": "string",
+                "enum": ThemeRegistry::global(cx).list_names(),
+            }))
+        }
+    }
+}
+
+inventory::submit! {
+    ParameterizedJsonSchema {
+        add_and_get_ref: |generator, _params, cx| {
+            replace_subschema::<settings::IconThemeName>(generator, || json_schema!({
+                "type": "string",
+                "enum": ThemeRegistry::global(cx)
+                    .list_icon_themes()
+                    .into_iter()
+                    .map(|icon_theme| icon_theme.name)
+                    .collect::<Vec<SharedString>>(),
+            }))
+        }
+    }
+}
+
+inventory::submit! {
+    ParameterizedJsonSchema {
+        add_and_get_ref: |generator, params, _cx| {
+            replace_subschema::<settings::FontFamilyName>(generator, || {
+                json_schema!({
+                    "type": "string",
+                    "enum": params.font_names,
+                })
+            })
+        }
+    }
+}
+
 /// Represents the selection of a theme, which can be either static or dynamic.
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 #[serde(untagged)]
@@ -277,24 +327,15 @@ pub enum ThemeSelection {
     },
 }
 
-// TODO: Rename ThemeMode -> ThemeAppearanceMode
-/// The mode use to select a theme.
-///
-/// `Light` and `Dark` will select their respective themes.
-///
-/// `System` will select the theme based on the system's appearance.
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ThemeMode {
-    /// Use the specified `light` theme.
-    Light,
-
-    /// Use the specified `dark` theme.
-    Dark,
-
-    /// Use the theme based on the system's appearance.
-    #[default]
-    System,
+impl From<settings::ThemeSelection> for ThemeSelection {
+    fn from(selection: settings::ThemeSelection) -> Self {
+        match selection {
+            settings::ThemeSelection::Static(theme) => ThemeSelection::Static(theme),
+            settings::ThemeSelection::Dynamic { mode, light, dark } => {
+                ThemeSelection::Dynamic { mode, light, dark }
+            }
+        }
+    }
 }
 
 impl ThemeSelection {
@@ -323,15 +364,13 @@ impl ThemeSelection {
 }
 
 /// Represents the selection of an icon theme, which can be either static or dynamic.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(untagged)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum IconThemeSelection {
     /// A static icon theme selection, represented by a single icon theme name.
     Static(IconThemeName),
     /// A dynamic icon theme selection, which can change based on the [`ThemeMode`].
     Dynamic {
         /// The mode used to determine which theme to use.
-        #[serde(default)]
         mode: ThemeMode,
         /// The icon theme to use for light mode.
         light: IconThemeName,
@@ -340,6 +379,17 @@ pub enum IconThemeSelection {
     },
 }
 
+impl From<settings::IconThemeSelection> for IconThemeSelection {
+    fn from(selection: settings::IconThemeSelection) -> Self {
+        match selection {
+            settings::IconThemeSelection::Static(theme) => IconThemeSelection::Static(theme),
+            settings::IconThemeSelection::Dynamic { mode, light, dark } => {
+                IconThemeSelection::Dynamic { mode, light, dark }
+            }
+        }
+    }
+}
+
 impl IconThemeSelection {
     /// Returns the icon theme name based on the given [`Appearance`].
     pub fn icon_theme(&self, system_appearance: Appearance) -> &str {
@@ -365,189 +415,117 @@ impl IconThemeSelection {
     }
 }
 
-/// Settings for rendering text in UI and text buffers.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
-pub struct ThemeSettingsContent {
-    /// The default font size for text in the UI.
-    #[serde(default)]
-    pub ui_font_size: Option<f32>,
-    /// The name of a font to use for rendering in the UI.
-    #[serde(default)]
-    pub ui_font_family: Option<FontFamilyName>,
-    /// The font fallbacks to use for rendering in the UI.
-    #[serde(default)]
-    #[schemars(default = "default_font_fallbacks")]
-    #[schemars(extend("uniqueItems" = true))]
-    pub ui_font_fallbacks: Option<Vec<FontFamilyName>>,
-    /// The OpenType features to enable for text in the UI.
-    #[serde(default)]
-    #[schemars(default = "default_font_features")]
-    pub ui_font_features: Option<FontFeatures>,
-    /// The weight of the UI font in CSS units from 100 to 900.
-    #[serde(default)]
-    pub ui_font_weight: Option<f32>,
-    /// The name of a font to use for rendering in text buffers.
-    #[serde(default)]
-    pub buffer_font_family: Option<FontFamilyName>,
-    /// The font fallbacks to use for rendering in text buffers.
-    #[serde(default)]
-    #[schemars(extend("uniqueItems" = true))]
-    pub buffer_font_fallbacks: Option<Vec<FontFamilyName>>,
-    /// The default font size for rendering in text buffers.
-    #[serde(default)]
-    pub buffer_font_size: Option<f32>,
-    /// The weight of the editor font in CSS units from 100 to 900.
-    #[serde(default)]
-    pub buffer_font_weight: Option<f32>,
-    /// The buffer's line height.
-    #[serde(default)]
-    pub buffer_line_height: Option<BufferLineHeight>,
-    /// The OpenType features to enable for rendering in text buffers.
-    #[serde(default)]
-    #[schemars(default = "default_font_features")]
-    pub buffer_font_features: Option<FontFeatures>,
-    /// The font size for the agent panel. Falls back to the UI font size if unset.
-    #[serde(default)]
-    pub agent_font_size: Option<Option<f32>>,
-    /// The name of the Zed theme to use.
-    #[serde(default)]
-    pub theme: Option<ThemeSelection>,
-    /// The name of the icon theme to use.
-    #[serde(default)]
-    pub icon_theme: Option<IconThemeSelection>,
-
-    /// UNSTABLE: Expect many elements to be broken.
-    ///
-    // Controls the density of the UI.
-    #[serde(rename = "unstable.ui_density", default)]
-    pub ui_density: Option<UiDensity>,
-
-    /// How much to fade out unused code.
-    #[serde(default)]
-    pub unnecessary_code_fade: Option<f32>,
-
-    /// EXPERIMENTAL: Overrides for the current theme.
-    ///
-    /// These values will override the ones on the current theme specified in `theme`.
-    #[serde(rename = "experimental.theme_overrides", default)]
-    pub experimental_theme_overrides: Option<ThemeStyleContent>,
-
-    /// Overrides per theme
-    ///
-    /// These values will override the ones on the specified theme
-    #[serde(default)]
-    pub theme_overrides: HashMap<String, ThemeStyleContent>,
-}
-
-fn default_font_features() -> Option<FontFeatures> {
-    Some(FontFeatures::default())
-}
-
-fn default_font_fallbacks() -> Option<FontFallbacks> {
-    Some(FontFallbacks::default())
-}
-
-impl ThemeSettingsContent {
-    /// Sets the theme for the given appearance to the theme with the specified name.
-    pub fn set_theme(&mut self, theme_name: impl Into<Arc<str>>, appearance: Appearance) {
-        if let Some(selection) = self.theme.as_mut() {
-            let theme_to_update = match selection {
-                ThemeSelection::Static(theme) => theme,
-                ThemeSelection::Dynamic { mode, light, dark } => match mode {
-                    ThemeMode::Light => light,
-                    ThemeMode::Dark => dark,
-                    ThemeMode::System => match appearance {
-                        Appearance::Light => light,
-                        Appearance::Dark => dark,
-                    },
+// impl ThemeSettingsContent {
+/// Sets the theme for the given appearance to the theme with the specified name.
+pub fn set_theme(
+    current: &mut SettingsContent,
+    theme_name: impl Into<Arc<str>>,
+    appearance: Appearance,
+) {
+    if let Some(selection) = current.theme.theme.as_mut() {
+        let theme_to_update = match selection {
+            settings::ThemeSelection::Static(theme) => theme,
+            settings::ThemeSelection::Dynamic { mode, light, dark } => match mode {
+                ThemeMode::Light => light,
+                ThemeMode::Dark => dark,
+                ThemeMode::System => match appearance {
+                    Appearance::Light => light,
+                    Appearance::Dark => dark,
                 },
-            };
-
-            *theme_to_update = ThemeName(theme_name.into());
-        } else {
-            self.theme = Some(ThemeSelection::Static(ThemeName(theme_name.into())));
-        }
-    }
+            },
+        };
 
-    /// Sets the icon theme for the given appearance to the icon theme with the specified name.
-    pub fn set_icon_theme(&mut self, icon_theme_name: String, appearance: Appearance) {
-        if let Some(selection) = self.icon_theme.as_mut() {
-            let icon_theme_to_update = match selection {
-                IconThemeSelection::Static(theme) => theme,
-                IconThemeSelection::Dynamic { mode, light, dark } => match mode {
-                    ThemeMode::Light => light,
-                    ThemeMode::Dark => dark,
-                    ThemeMode::System => match appearance {
-                        Appearance::Light => light,
-                        Appearance::Dark => dark,
-                    },
+        *theme_to_update = ThemeName(theme_name.into());
+    } else {
+        current.theme.theme = Some(settings::ThemeSelection::Static(ThemeName(
+            theme_name.into(),
+        )));
+    }
+}
+
+/// Sets the icon theme for the given appearance to the icon theme with the specified name.
+pub fn set_icon_theme(
+    current: &mut SettingsContent,
+    icon_theme_name: String,
+    appearance: Appearance,
+) {
+    if let Some(selection) = current.theme.icon_theme.as_mut() {
+        let icon_theme_to_update = match selection {
+            settings::IconThemeSelection::Static(theme) => theme,
+            settings::IconThemeSelection::Dynamic { mode, light, dark } => match mode {
+                ThemeMode::Light => light,
+                ThemeMode::Dark => dark,
+                ThemeMode::System => match appearance {
+                    Appearance::Light => light,
+                    Appearance::Dark => dark,
                 },
-            };
-
-            *icon_theme_to_update = IconThemeName(icon_theme_name.into());
-        } else {
-            self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
-                icon_theme_name.into(),
-            )));
-        }
-    }
+            },
+        };
 
-    /// Sets the mode for the theme.
-    pub fn set_mode(&mut self, mode: ThemeMode) {
-        if let Some(selection) = self.theme.as_mut() {
-            match selection {
-                ThemeSelection::Static(theme) => {
-                    // If the theme was previously set to a single static theme,
-                    // we don't know whether it was a light or dark theme, so we
-                    // just use it for both.
-                    self.theme = Some(ThemeSelection::Dynamic {
-                        mode,
-                        light: theme.clone(),
-                        dark: theme.clone(),
-                    });
-                }
-                ThemeSelection::Dynamic {
-                    mode: mode_to_update,
-                    ..
-                } => *mode_to_update = mode,
+        *icon_theme_to_update = IconThemeName(icon_theme_name.into());
+    } else {
+        current.theme.icon_theme = Some(settings::IconThemeSelection::Static(IconThemeName(
+            icon_theme_name.into(),
+        )));
+    }
+}
+
+/// Sets the mode for the theme.
+pub fn set_mode(content: &mut SettingsContent, mode: ThemeMode) {
+    let theme = content.theme.as_mut();
+
+    if let Some(selection) = theme.theme.as_mut() {
+        match selection {
+            settings::ThemeSelection::Static(theme) => {
+                // If the theme was previously set to a single static theme,
+                // we don't know whether it was a light or dark theme, so we
+                // just use it for both.
+                *selection = settings::ThemeSelection::Dynamic {
+                    mode,
+                    light: theme.clone(),
+                    dark: theme.clone(),
+                };
             }
-        } else {
-            self.theme = Some(ThemeSelection::Dynamic {
-                mode,
-                light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()),
-                dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()),
-            });
+            settings::ThemeSelection::Dynamic {
+                mode: mode_to_update,
+                ..
+            } => *mode_to_update = mode,
         }
-
-        if let Some(selection) = self.icon_theme.as_mut() {
-            match selection {
-                IconThemeSelection::Static(icon_theme) => {
-                    // If the icon theme was previously set to a single static
-                    // theme, we don't know whether it was a light or dark
-                    // theme, so we just use it for both.
-                    self.icon_theme = Some(IconThemeSelection::Dynamic {
-                        mode,
-                        light: icon_theme.clone(),
-                        dark: icon_theme.clone(),
-                    });
-                }
-                IconThemeSelection::Dynamic {
-                    mode: mode_to_update,
-                    ..
-                } => *mode_to_update = mode,
+    } else {
+        theme.theme = Some(settings::ThemeSelection::Dynamic {
+            mode,
+            light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()),
+            dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()),
+        });
+    }
+
+    if let Some(selection) = theme.icon_theme.as_mut() {
+        match selection {
+            settings::IconThemeSelection::Static(icon_theme) => {
+                // If the icon theme was previously set to a single static
+                // theme, we don't know whether it was a light or dark
+                // theme, so we just use it for both.
+                *selection = settings::IconThemeSelection::Dynamic {
+                    mode,
+                    light: icon_theme.clone(),
+                    dark: icon_theme.clone(),
+                };
             }
-        } else {
-            self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
-                DEFAULT_ICON_THEME_NAME.into(),
-            )));
+            settings::IconThemeSelection::Dynamic {
+                mode: mode_to_update,
+                ..
+            } => *mode_to_update = mode,
         }
+    } else {
+        theme.icon_theme = Some(settings::IconThemeSelection::Static(IconThemeName(
+            DEFAULT_ICON_THEME_NAME.into(),
+        )));
     }
 }
+// }
 
 /// The buffer's line height.
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
-#[serde(rename_all = "snake_case")]
+#[derive(Clone, Copy, Debug, PartialEq, Default)]
 pub enum BufferLineHeight {
     /// A less dense line height.
     #[default]
@@ -555,21 +533,19 @@ pub enum BufferLineHeight {
     /// The default line height.
     Standard,
     /// A custom line height, where 1.0 is the font's height. Must be at least 1.0.
-    Custom(#[serde(deserialize_with = "deserialize_line_height")] f32),
+    Custom(f32),
 }
 
-fn deserialize_line_height<'de, D>(deserializer: D) -> Result<f32, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    let value = f32::deserialize(deserializer)?;
-    if value < 1.0 {
-        return Err(serde::de::Error::custom(
-            "buffer_line_height.custom must be at least 1.0",
-        ));
+impl From<settings::BufferLineHeight> for BufferLineHeight {
+    fn from(value: settings::BufferLineHeight) -> Self {
+        match value {
+            settings::BufferLineHeight::Comfortable => BufferLineHeight::Comfortable,
+            settings::BufferLineHeight::Standard => BufferLineHeight::Standard,
+            settings::BufferLineHeight::Custom(line_height) => {
+                BufferLineHeight::Custom(line_height)
+            }
+        }
     }
-
-    Ok(value)
 }
 
 impl BufferLineHeight {
@@ -681,24 +657,22 @@ impl ThemeSettings {
         }
     }
 
-    fn modify_theme(base_theme: &mut Theme, theme_overrides: &ThemeStyleContent) {
+    fn modify_theme(base_theme: &mut Theme, theme_overrides: &settings::ThemeStyleContent) {
         if let Some(window_background_appearance) = theme_overrides.window_background_appearance {
             base_theme.styles.window_background_appearance = window_background_appearance.into();
         }
+        let status_color_refinement = status_colors_refinement(&theme_overrides.status);
 
-        base_theme
-            .styles
-            .colors
-            .refine(&theme_overrides.theme_colors_refinement());
-        base_theme
-            .styles
-            .status
-            .refine(&theme_overrides.status_colors_refinement());
+        base_theme.styles.colors.refine(&theme_colors_refinement(
+            &theme_overrides.colors,
+            &status_color_refinement,
+        ));
+        base_theme.styles.status.refine(&status_color_refinement);
         base_theme.styles.player.merge(&theme_overrides.players);
         base_theme.styles.accents.merge(&theme_overrides.accents);
         base_theme.styles.syntax = SyntaxTheme::merge(
             base_theme.styles.syntax.clone(),
-            theme_overrides.syntax_overrides(),
+            syntax_overrides(&theme_overrides),
         );
     }
 
@@ -818,294 +792,178 @@ fn clamp_font_weight(weight: f32) -> FontWeight {
     FontWeight(weight.clamp(100., 950.))
 }
 
-impl settings::Settings for ThemeSettings {
-    type FileContent = ThemeSettingsContent;
+/// font fallback from settings
+pub fn font_fallbacks_from_settings(
+    fallbacks: Option<Vec<settings::FontFamilyName>>,
+) -> Option<FontFallbacks> {
+    fallbacks.map(|fallbacks| {
+        FontFallbacks::from_fonts(
+            fallbacks
+                .into_iter()
+                .map(|font_family| font_family.0.to_string())
+                .collect(),
+        )
+    })
+}
 
-    fn load(sources: SettingsSources<Self::FileContent>, cx: &mut App) -> Result<Self> {
+impl settings::Settings for ThemeSettings {
+    fn from_defaults(content: &settings::SettingsContent, cx: &mut App) -> Self {
+        let content = &content.theme;
+        // todo(settings_refactor). This should *not* require cx...
         let themes = ThemeRegistry::default_global(cx);
         let system_appearance = SystemAppearance::default_global(cx);
-
-        fn font_fallbacks_from_settings(
-            fallbacks: Option<Vec<FontFamilyName>>,
-        ) -> Option<FontFallbacks> {
-            fallbacks.map(|fallbacks| {
-                FontFallbacks::from_fonts(
-                    fallbacks
-                        .into_iter()
-                        .map(|font_family| font_family.0.to_string())
-                        .collect(),
-                )
-            })
-        }
-
-        let defaults = sources.default;
-        let mut this = Self {
-            ui_font_size: defaults.ui_font_size.unwrap().into(),
+        let theme_selection: ThemeSelection = content.theme.clone().unwrap().into();
+        let icon_theme_selection: IconThemeSelection = content.icon_theme.clone().unwrap().into();
+        Self {
+            ui_font_size: content.ui_font_size.unwrap().into(),
             ui_font: Font {
-                family: defaults.ui_font_family.as_ref().unwrap().0.clone().into(),
-                features: defaults.ui_font_features.clone().unwrap(),
-                fallbacks: font_fallbacks_from_settings(defaults.ui_font_fallbacks.clone()),
-                weight: defaults.ui_font_weight.map(FontWeight).unwrap(),
+                family: content.ui_font_family.as_ref().unwrap().0.clone().into(),
+                features: content.ui_font_features.clone().unwrap(),
+                fallbacks: font_fallbacks_from_settings(content.ui_font_fallbacks.clone()),
+                weight: content.ui_font_weight.map(FontWeight).unwrap(),
                 style: Default::default(),
             },
             buffer_font: Font {
-                family: defaults
+                family: content
                     .buffer_font_family
                     .as_ref()
                     .unwrap()
                     .0
                     .clone()
                     .into(),
-                features: defaults.buffer_font_features.clone().unwrap(),
-                fallbacks: font_fallbacks_from_settings(defaults.buffer_font_fallbacks.clone()),
-                weight: defaults.buffer_font_weight.map(FontWeight).unwrap(),
+                features: content.buffer_font_features.clone().unwrap(),
+                fallbacks: font_fallbacks_from_settings(content.buffer_font_fallbacks.clone()),
+                weight: content.buffer_font_weight.map(FontWeight).unwrap(),
                 style: FontStyle::default(),
             },
-            buffer_font_size: defaults.buffer_font_size.unwrap().into(),
-            buffer_line_height: defaults.buffer_line_height.unwrap(),
-            agent_font_size: defaults.agent_font_size.flatten().map(Into::into),
-            theme_selection: defaults.theme.clone(),
+            buffer_font_size: content.buffer_font_size.unwrap().into(),
+            buffer_line_height: content.buffer_line_height.unwrap().into(),
+            agent_font_size: content.agent_font_size.flatten().map(Into::into),
             active_theme: themes
-                .get(defaults.theme.as_ref().unwrap().theme(*system_appearance))
+                .get(theme_selection.theme(*system_appearance))
                 .or(themes.get(&zed_default_dark().name))
                 .unwrap(),
+            theme_selection: Some(theme_selection),
             experimental_theme_overrides: None,
             theme_overrides: HashMap::default(),
-            icon_theme_selection: defaults.icon_theme.clone(),
-            active_icon_theme: defaults
-                .icon_theme
-                .as_ref()
-                .and_then(|selection| {
-                    themes
-                        .get_icon_theme(selection.icon_theme(*system_appearance))
-                        .ok()
-                })
-                .unwrap_or_else(|| themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap()),
-            ui_density: defaults.ui_density.unwrap_or(UiDensity::Default),
-            unnecessary_code_fade: defaults.unnecessary_code_fade.unwrap_or(0.0),
-        };
-
-        for value in sources
-            .user
-            .into_iter()
-            .chain(sources.release_channel)
-            .chain(sources.operating_system)
-            .chain(sources.profile)
-            .chain(sources.server)
-        {
-            if let Some(value) = value.ui_density {
-                this.ui_density = value;
-            }
-
-            if let Some(value) = value.buffer_font_family.clone() {
-                this.buffer_font.family = value.0.into();
-            }
-            if let Some(value) = value.buffer_font_features.clone() {
-                this.buffer_font.features = value;
-            }
-            if let Some(value) = value.buffer_font_fallbacks.clone() {
-                this.buffer_font.fallbacks = font_fallbacks_from_settings(Some(value));
-            }
-            if let Some(value) = value.buffer_font_weight {
-                this.buffer_font.weight = clamp_font_weight(value);
-            }
+            active_icon_theme: themes
+                .get_icon_theme(icon_theme_selection.icon_theme(*system_appearance))
+                .ok()
+                .unwrap(),
+            icon_theme_selection: Some(icon_theme_selection),
+            ui_density: content.ui_density.unwrap_or_default().into(),
+            unnecessary_code_fade: content.unnecessary_code_fade.unwrap(),
+        }
+    }
 
-            if let Some(value) = value.ui_font_family.clone() {
-                this.ui_font.family = value.0.into();
-            }
-            if let Some(value) = value.ui_font_features.clone() {
-                this.ui_font.features = value;
-            }
-            if let Some(value) = value.ui_font_fallbacks.clone() {
-                this.ui_font.fallbacks = font_fallbacks_from_settings(Some(value));
-            }
-            if let Some(value) = value.ui_font_weight {
-                this.ui_font.weight = clamp_font_weight(value);
-            }
+    fn refine(&mut self, content: &SettingsContent, cx: &mut App) {
+        let value = &content.theme;
 
-            if let Some(value) = &value.theme {
-                this.theme_selection = Some(value.clone());
+        let themes = ThemeRegistry::default_global(cx);
+        let system_appearance = SystemAppearance::default_global(cx);
 
-                let theme_name = value.theme(*system_appearance);
+        self.ui_density
+            .merge_from(&value.ui_density.map(Into::into));
 
-                match themes.get(theme_name) {
-                    Ok(theme) => {
-                        this.active_theme = theme;
-                    }
-                    Err(err @ ThemeNotFoundError(_)) => {
-                        if themes.extensions_loaded() {
-                            log::error!("{err}");
-                        }
-                    }
-                }
-            }
+        if let Some(value) = value.buffer_font_family.clone() {
+            self.buffer_font.family = value.0.into();
+        }
+        if let Some(value) = value.buffer_font_features.clone() {
+            self.buffer_font.features = value;
+        }
+        if let Some(value) = value.buffer_font_fallbacks.clone() {
+            self.buffer_font.fallbacks = font_fallbacks_from_settings(Some(value));
+        }
+        if let Some(value) = value.buffer_font_weight {
+            self.buffer_font.weight = clamp_font_weight(value);
+        }
 
-            this.experimental_theme_overrides
-                .clone_from(&value.experimental_theme_overrides);
-            this.theme_overrides.clone_from(&value.theme_overrides);
-            this.apply_theme_overrides();
+        if let Some(value) = value.ui_font_family.clone() {
+            self.ui_font.family = value.0.into();
+        }
+        if let Some(value) = value.ui_font_features.clone() {
+            self.ui_font.features = value;
+        }
+        if let Some(value) = value.ui_font_fallbacks.clone() {
+            self.ui_font.fallbacks = font_fallbacks_from_settings(Some(value));
+        }
+        if let Some(value) = value.ui_font_weight {
+            self.ui_font.weight = clamp_font_weight(value);
+        }
 
-            if let Some(value) = &value.icon_theme {
-                this.icon_theme_selection = Some(value.clone());
+        if let Some(value) = &value.theme {
+            self.theme_selection = Some(value.clone().into());
 
-                let icon_theme_name = value.icon_theme(*system_appearance);
+            let theme_name = self
+                .theme_selection
+                .as_ref()
+                .unwrap()
+                .theme(*system_appearance);
 
-                match themes.get_icon_theme(icon_theme_name) {
-                    Ok(icon_theme) => {
-                        this.active_icon_theme = icon_theme;
-                    }
-                    Err(err @ IconThemeNotFoundError(_)) => {
-                        if themes.extensions_loaded() {
-                            log::error!("{err}");
-                        }
+            match themes.get(theme_name) {
+                Ok(theme) => {
+                    self.active_theme = theme;
+                }
+                Err(err @ ThemeNotFoundError(_)) => {
+                    if themes.extensions_loaded() {
+                        log::error!("{err}");
                     }
                 }
             }
-
-            merge(
-                &mut this.ui_font_size,
-                value.ui_font_size.map(Into::into).map(clamp_font_size),
-            );
-            merge(
-                &mut this.buffer_font_size,
-                value.buffer_font_size.map(Into::into).map(clamp_font_size),
-            );
-            merge(
-                &mut this.agent_font_size,
-                value
-                    .agent_font_size
-                    .map(|value| value.map(Into::into).map(clamp_font_size)),
-            );
-
-            merge(&mut this.buffer_line_height, value.buffer_line_height);
-
-            // Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely.
-            merge(&mut this.unnecessary_code_fade, value.unnecessary_code_fade);
-            this.unnecessary_code_fade = this.unnecessary_code_fade.clamp(0.0, 0.9);
         }
 
-        Ok(this)
-    }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
-        vscode.f32_setting("editor.fontWeight", &mut current.buffer_font_weight);
-        vscode.f32_setting("editor.fontSize", &mut current.buffer_font_size);
-        if let Some(font) = vscode.read_string("editor.font") {
-            current.buffer_font_family = Some(FontFamilyName(font.into()));
-        }
-        // TODO: possibly map editor.fontLigatures to buffer_font_features?
-    }
-}
-
-/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(transparent)]
-pub struct ThemeName(pub Arc<str>);
-
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, _params, cx| {
-            replace_subschema::<ThemeName>(generator, || json_schema!({
-                "type": "string",
-                "enum": ThemeRegistry::global(cx).list_names(),
-            }))
-        }
-    }
-}
+        self.experimental_theme_overrides
+            .clone_from(&value.experimental_theme_overrides);
+        self.theme_overrides.clone_from(&value.theme_overrides);
 
-/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at
-/// runtime.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(transparent)]
-pub struct IconThemeName(pub Arc<str>);
+        self.apply_theme_overrides();
 
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, _params, cx| {
-            replace_subschema::<IconThemeName>(generator, || json_schema!({
-                "type": "string",
-                "enum": ThemeRegistry::global(cx)
-                    .list_icon_themes()
-                    .into_iter()
-                    .map(|icon_theme| icon_theme.name)
-                    .collect::<Vec<SharedString>>(),
-            }))
-        }
-    }
-}
+        if let Some(value) = &value.icon_theme {
+            self.icon_theme_selection = Some(value.clone().into());
 
-/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at
-/// runtime.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(transparent)]
-pub struct FontFamilyName(pub Arc<str>);
+            let icon_theme_name = self
+                .icon_theme_selection
+                .as_ref()
+                .unwrap()
+                .icon_theme(*system_appearance);
 
-inventory::submit! {
-    ParameterizedJsonSchema {
-        add_and_get_ref: |generator, params, _cx| {
-            replace_subschema::<FontFamilyName>(generator, || {
-                json_schema!({
-                    "type": "string",
-                    "enum": params.font_names,
-                })
-            })
+            match themes.get_icon_theme(icon_theme_name) {
+                Ok(icon_theme) => {
+                    self.active_icon_theme = icon_theme;
+                }
+                Err(err @ IconThemeNotFoundError(_)) => {
+                    if themes.extensions_loaded() {
+                        log::error!("{err}");
+                    }
+                }
+            }
         }
-    }
-}
 
-fn merge<T: Copy>(target: &mut T, value: Option<T>) {
-    if let Some(value) = value {
-        *target = value;
-    }
-}
+        self.ui_font_size
+            .merge_from(&value.ui_font_size.map(Into::into).map(clamp_font_size));
+        self.buffer_font_size
+            .merge_from(&value.buffer_font_size.map(Into::into).map(clamp_font_size));
+        self.agent_font_size.merge_from(
+            &value
+                .agent_font_size
+                .map(|value| value.map(Into::into).map(clamp_font_size)),
+        );
 
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use serde_json::json;
+        self.buffer_line_height
+            .merge_from(&value.buffer_line_height.map(Into::into));
 
-    #[test]
-    fn test_buffer_line_height_deserialize_valid() {
-        assert_eq!(
-            serde_json::from_value::<BufferLineHeight>(json!("comfortable")).unwrap(),
-            BufferLineHeight::Comfortable
-        );
-        assert_eq!(
-            serde_json::from_value::<BufferLineHeight>(json!("standard")).unwrap(),
-            BufferLineHeight::Standard
-        );
-        assert_eq!(
-            serde_json::from_value::<BufferLineHeight>(json!({"custom": 1.0})).unwrap(),
-            BufferLineHeight::Custom(1.0)
-        );
-        assert_eq!(
-            serde_json::from_value::<BufferLineHeight>(json!({"custom": 1.5})).unwrap(),
-            BufferLineHeight::Custom(1.5)
-        );
+        // Clamp the `unnecessary_code_fade` to ensure text can't disappear entirely.
+        self.unnecessary_code_fade
+            .merge_from(&value.unnecessary_code_fade);
+        self.unnecessary_code_fade = self.unnecessary_code_fade.clamp(0.0, 0.9);
     }
 
-    #[test]
-    fn test_buffer_line_height_deserialize_invalid() {
-        assert!(
-            serde_json::from_value::<BufferLineHeight>(json!({"custom": 0.99}))
-                .err()
-                .unwrap()
-                .to_string()
-                .contains("buffer_line_height.custom must be at least 1.0")
-        );
-        assert!(
-            serde_json::from_value::<BufferLineHeight>(json!({"custom": 0.0}))
-                .err()
-                .unwrap()
-                .to_string()
-                .contains("buffer_line_height.custom must be at least 1.0")
-        );
-        assert!(
-            serde_json::from_value::<BufferLineHeight>(json!({"custom": -1.0}))
-                .err()
-                .unwrap()
-                .to_string()
-                .contains("buffer_line_height.custom must be at least 1.0")
-        );
+    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
+        vscode.f32_setting("editor.fontWeight", &mut current.theme.buffer_font_weight);
+        vscode.f32_setting("editor.fontSize", &mut current.theme.buffer_font_size);
+        if let Some(font) = vscode.read_string("editor.font") {
+            current.theme.buffer_font_family = Some(FontFamilyName(font.into()));
+        }
+        // TODO: possibly map editor.fontLigatures to buffer_font_features?
     }
 }

crates/theme/src/styles/accents.rs πŸ”—

@@ -2,8 +2,8 @@ use gpui::Hsla;
 use serde::Deserialize;
 
 use crate::{
-    AccentContent, amber, blue, cyan, gold, grass, indigo, iris, jade, lime, orange, pink, purple,
-    tomato, try_parse_color,
+    amber, blue, cyan, gold, grass, indigo, iris, jade, lime, orange, pink, purple, tomato,
+    try_parse_color,
 };
 
 /// A collection of colors that are used to color indent aware lines in the editor.
@@ -66,7 +66,7 @@ impl AccentColors {
     }
 
     /// Merges the given accent colors into this [`AccentColors`] instance.
-    pub fn merge(&mut self, accent_colors: &[AccentContent]) {
+    pub fn merge(&mut self, accent_colors: &[settings::AccentContent]) {
         if accent_colors.is_empty() {
             return;
         }

crates/theme/src/styles/players.rs πŸ”—

@@ -3,9 +3,7 @@
 use gpui::Hsla;
 use serde::Deserialize;
 
-use crate::{
-    PlayerColorContent, amber, blue, jade, lime, orange, pink, purple, red, try_parse_color,
-};
+use crate::{amber, blue, jade, lime, orange, pink, purple, red, try_parse_color};
 
 #[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq)]
 pub struct PlayerColor {
@@ -152,7 +150,7 @@ impl PlayerColors {
     }
 
     /// Merges the given player colors into this [`PlayerColors`] instance.
-    pub fn merge(&mut self, user_player_colors: &[PlayerColorContent]) {
+    pub fn merge(&mut self, user_player_colors: &[settings::PlayerColorContent]) {
         if user_player_colors.is_empty() {
             return;
         }

crates/theme/src/theme.rs πŸ”—

@@ -44,6 +44,10 @@ pub use crate::scale::*;
 pub use crate::schema::*;
 pub use crate::settings::*;
 pub use crate::styles::*;
+pub use ::settings::{
+    FontStyleContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent,
+    ThemeStyleContent,
+};
 
 /// Defines window border radius for platforms that use client side decorations.
 pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
@@ -178,7 +182,7 @@ impl ThemeFamily {
             AppearanceContent::Light => StatusColors::light(),
             AppearanceContent::Dark => StatusColors::dark(),
         };
-        let mut status_colors_refinement = theme.style.status_colors_refinement();
+        let mut status_colors_refinement = status_colors_refinement(&theme.style.status);
         apply_status_color_defaults(&mut status_colors_refinement);
         refined_status_colors.refine(&status_colors_refinement);
 
@@ -192,7 +196,8 @@ impl ThemeFamily {
             AppearanceContent::Light => ThemeColors::light(),
             AppearanceContent::Dark => ThemeColors::dark(),
         };
-        let mut theme_colors_refinement = theme.style.theme_colors_refinement();
+        let mut theme_colors_refinement =
+            theme_colors_refinement(&theme.style.colors, &status_colors_refinement);
         apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors);
         refined_theme_colors.refine(&theme_colors_refinement);
 

crates/theme_importer/Cargo.toml πŸ”—

@@ -11,6 +11,7 @@ workspace = true
 [dependencies]
 anyhow.workspace = true
 clap = { workspace = true, features = ["derive"] }
+collections.workspace = true
 gpui.workspace = true
 indexmap.workspace = true
 log.workspace = true

crates/theme_importer/src/main.rs πŸ”—

@@ -7,7 +7,7 @@ use std::path::PathBuf;
 
 use anyhow::{Context as _, Result};
 use clap::Parser;
-use indexmap::IndexMap;
+use collections::IndexMap;
 use log::LevelFilter;
 use serde::Deserialize;
 use simplelog::ColorChoice;
@@ -137,7 +137,7 @@ fn main() -> Result<()> {
         file_name: "".to_string(),
     };
 
-    let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata, IndexMap::new());
+    let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata, IndexMap::default());
 
     let theme = converter.convert()?;
     let mut theme = serde_json::to_value(theme).unwrap();

crates/theme_importer/src/vscode/converter.rs πŸ”—

@@ -1,9 +1,9 @@
 use anyhow::Result;
-use indexmap::IndexMap;
+use collections::IndexMap;
 use strum::IntoEnumIterator;
 use theme::{
     FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent,
-    ThemeColorsContent, ThemeContent, ThemeStyleContent,
+    ThemeColorsContent, ThemeContent, ThemeStyleContent, WindowBackgroundContent,
 };
 
 use crate::ThemeMetadata;
@@ -56,7 +56,7 @@ impl VsCodeThemeConverter {
             name: self.theme_metadata.name,
             appearance,
             style: ThemeStyleContent {
-                window_background_appearance: Some(theme::WindowBackgroundContent::Opaque),
+                window_background_appearance: Some(WindowBackgroundContent::Opaque),
                 accents: Vec::new(), //TODO can we read this from the theme?
                 colors: theme_colors,
                 status: status_colors,
@@ -212,7 +212,7 @@ impl VsCodeThemeConverter {
     }
 
     fn convert_syntax_theme(&self) -> Result<IndexMap<String, HighlightStyleContent>> {
-        let mut highlight_styles = IndexMap::new();
+        let mut highlight_styles = IndexMap::default();
 
         for syntax_token in ZedSyntaxToken::iter() {
             let override_match = self

crates/theme_selector/src/icon_theme_selector.rs πŸ”—

@@ -180,8 +180,8 @@ impl PickerDelegate for IconThemeSelectorDelegate {
 
         let appearance = Appearance::from(window.appearance());
 
-        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
-            settings.set_icon_theme(theme_name.to_string(), appearance);
+        update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            theme::set_icon_theme(settings, theme_name.to_string(), appearance);
         });
 
         self.selector

crates/theme_selector/src/theme_selector.rs πŸ”—

@@ -238,8 +238,8 @@ impl PickerDelegate for ThemeSelectorDelegate {
 
         let appearance = Appearance::from(window.appearance());
 
-        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
-            settings.set_theme(theme_name.to_string(), appearance);
+        update_settings_file(self.fs.clone(), cx, move |settings, _| {
+            theme::set_theme(settings, theme_name.to_string(), appearance);
         });
 
         self.selector

crates/title_bar/src/title_bar_settings.rs πŸ”—

@@ -1,17 +1,9 @@
-use db::anyhow;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+pub use settings::TitleBarVisibility;
+use settings::{Settings, SettingsContent};
+use ui::App;
+use util::MergeFrom;
 
-#[derive(Copy, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-pub enum TitleBarVisibility {
-    Always,
-    Never,
-    HideInFullScreen,
-}
-
-#[derive(Copy, Clone, Deserialize, Debug)]
+#[derive(Copy, Clone, Debug)]
 pub struct TitleBarSettings {
     pub show: TitleBarVisibility,
     pub show_branch_icon: bool,
@@ -23,55 +15,38 @@ pub struct TitleBarSettings {
     pub show_menus: bool,
 }
 
-#[derive(
-    Copy, Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey,
-)]
-#[settings_ui(group = "Title Bar")]
-#[settings_key(key = "title_bar")]
-pub struct TitleBarSettingsContent {
-    /// Controls when the title bar is visible: "always" | "never" | "hide_in_full_screen".
-    ///
-    /// Default: "always"
-    pub show: Option<TitleBarVisibility>,
-    /// Whether to show the branch icon beside branch switcher in the title bar.
-    ///
-    /// Default: false
-    pub show_branch_icon: Option<bool>,
-    /// Whether to show onboarding banners in the title bar.
-    ///
-    /// Default: true
-    pub show_onboarding_banner: Option<bool>,
-    /// Whether to show user avatar in the title bar.
-    ///
-    /// Default: true
-    pub show_user_picture: Option<bool>,
-    /// Whether to show the branch name button in the titlebar.
-    ///
-    /// Default: true
-    pub show_branch_name: Option<bool>,
-    /// Whether to show the project host and name in the titlebar.
-    ///
-    /// Default: true
-    pub show_project_items: Option<bool>,
-    /// Whether to show the sign in button in the title bar.
-    ///
-    /// Default: true
-    pub show_sign_in: Option<bool>,
-    /// Whether to show the menus in the title bar.
-    ///
-    /// Default: false
-    pub show_menus: Option<bool>,
-}
-
 impl Settings for TitleBarSettings {
-    type FileContent = TitleBarSettingsContent;
+    fn from_defaults(s: &SettingsContent, _: &mut App) -> Self {
+        let content = s.title_bar.clone().unwrap();
+        TitleBarSettings {
+            show: content.show.unwrap(),
+            show_branch_icon: content.show_branch_icon.unwrap(),
+            show_onboarding_banner: content.show_onboarding_banner.unwrap(),
+            show_user_picture: content.show_user_picture.unwrap(),
+            show_branch_name: content.show_branch_name.unwrap(),
+            show_project_items: content.show_project_items.unwrap(),
+            show_sign_in: content.show_sign_in.unwrap(),
+            show_menus: content.show_menus.unwrap(),
+        }
+    }
+
+    fn refine(&mut self, s: &SettingsContent, _: &mut App) {
+        let Some(content) = &s.title_bar else {
+            return;
+        };
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut gpui::App) -> anyhow::Result<Self>
-    where
-        Self: Sized,
-    {
-        sources.json_merge()
+        self.show.merge_from(&content.show);
+        self.show_branch_icon.merge_from(&content.show_branch_icon);
+        self.show_onboarding_banner
+            .merge_from(&content.show_onboarding_banner);
+        self.show_user_picture
+            .merge_from(&content.show_user_picture);
+        self.show_branch_name.merge_from(&content.show_branch_name);
+        self.show_project_items
+            .merge_from(&content.show_project_items);
+        self.show_sign_in.merge_from(&content.show_sign_in);
+        self.show_menus.merge_from(&content.show_menus);
     }
 
-    fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut Self::FileContent) {}
+    fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut SettingsContent) {}
 }

crates/ui/src/components/scrollbar.rs πŸ”—

@@ -45,6 +45,17 @@ pub mod scrollbars {
         Never,
     }
 
+    impl From<settings::ShowScrollbar> for ShowScrollbar {
+        fn from(value: settings::ShowScrollbar) -> Self {
+            match value {
+                settings::ShowScrollbar::Auto => ShowScrollbar::Auto,
+                settings::ShowScrollbar::System => ShowScrollbar::System,
+                settings::ShowScrollbar::Always => ShowScrollbar::Always,
+                settings::ShowScrollbar::Never => ShowScrollbar::Never,
+            }
+        }
+    }
+
     impl ShowScrollbar {
         pub(super) fn show(&self) -> bool {
             *self != Self::Never

crates/ui_macros/src/dynamic_spacing.rs πŸ”—

@@ -66,9 +66,9 @@ pub fn derive_spacing(input: TokenStream) -> TokenStream {
                     let n = n.base10_parse::<f32>().unwrap();
                     quote! {
                         DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density {
-                            UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX,
-                            UiDensity::Default => #n / BASE_REM_SIZE_IN_PX,
-                            UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX,
+                            ::theme::UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX,
+                            ::theme::UiDensity::Default => #n / BASE_REM_SIZE_IN_PX,
+                            ::theme::UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX,
                         }
                     }
                 }
@@ -78,9 +78,9 @@ pub fn derive_spacing(input: TokenStream) -> TokenStream {
                     let c = c.base10_parse::<f32>().unwrap();
                     quote! {
                         DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density {
-                            UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX,
-                            UiDensity::Default => #b / BASE_REM_SIZE_IN_PX,
-                            UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX,
+                            ::theme::UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX,
+                            ::theme::UiDensity::Default => #b / BASE_REM_SIZE_IN_PX,
+                            ::theme::UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX,
                         }
                     }
                 }

crates/util/src/util.rs πŸ”—

@@ -1390,3 +1390,21 @@ Line 3"#
         assert_eq!(result[1], (10..15, "world")); // 'πŸ¦€' is 4 bytes
     }
 }
+
+pub fn refine<T: Clone>(dest: &mut T, src: &Option<T>) {
+    if let Some(src) = src {
+        *dest = src.clone()
+    }
+}
+
+pub trait MergeFrom: Sized + Clone {
+    fn merge_from(&mut self, src: &Option<Self>);
+}
+
+impl<T: Clone> MergeFrom for T {
+    fn merge_from(&mut self, src: &Option<Self>) {
+        if let Some(src) = src {
+            *self = src.clone();
+        }
+    }
+}

crates/vim/src/digraph.rs πŸ”—

@@ -224,7 +224,6 @@ mod test {
     use settings::SettingsStore;
 
     use crate::{
-        VimSettings,
         state::Mode,
         test::{NeovimBackedTestContext, VimTestContext},
     };
@@ -294,11 +293,11 @@ mod test {
         let mut cx: VimTestContext = VimTestContext::new(cx, true).await;
 
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<VimSettings>(cx, |s| {
+            store.update_user_settings(cx, |s| {
                 let mut custom_digraphs = HashMap::default();
                 custom_digraphs.insert("|-".into(), "⊒".into());
                 custom_digraphs.insert(":)".into(), "πŸ‘¨β€πŸ’»".into());
-                s.custom_digraphs = Some(custom_digraphs);
+                s.vim.get_or_insert_default().custom_digraphs = Some(custom_digraphs);
             });
         });
 

crates/vim/src/normal.rs πŸ”—

@@ -989,11 +989,10 @@ impl Vim {
 mod test {
     use gpui::{KeyBinding, TestAppContext, UpdateGlobal};
     use indoc::indoc;
-    use language::language_settings::AllLanguageSettings;
     use settings::SettingsStore;
 
     use crate::{
-        VimSettings, motion,
+        motion,
         state::Mode::{self},
         test::{NeovimBackedTestContext, VimTestContext},
     };
@@ -1724,8 +1723,8 @@ mod test {
     async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<VimSettings>(cx, |s| {
-                s.use_smartcase_find = Some(true);
+            store.update_user_settings(cx, |s| {
+                s.vim.get_or_insert_default().use_smartcase_find = Some(true);
             });
         });
 
@@ -1891,8 +1890,12 @@ mod test {
 
         cx.update(|_, cx| {
             SettingsStore::update_global(cx, |settings, cx| {
-                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                    settings.defaults.preferred_line_length = Some(5);
+                settings.update_user_settings(cx, |settings| {
+                    settings
+                        .project
+                        .all_languages
+                        .defaults
+                        .preferred_line_length = Some(5);
                 });
             })
         });

crates/vim/src/normal/paste.rs πŸ”—

@@ -311,17 +311,13 @@ impl Vim {
 #[cfg(test)]
 mod test {
     use crate::{
-        UseSystemClipboard, VimSettings,
         state::{Mode, Register},
         test::{NeovimBackedTestContext, VimTestContext},
     };
     use gpui::ClipboardItem;
     use indoc::indoc;
-    use language::{
-        LanguageName,
-        language_settings::{AllLanguageSettings, LanguageSettingsContent},
-    };
-    use settings::SettingsStore;
+    use language::{LanguageName, language_settings::LanguageSettingsContent};
+    use settings::{SettingsStore, UseSystemClipboard};
 
     #[gpui::test]
     async fn test_paste(cx: &mut gpui::TestAppContext) {
@@ -408,8 +404,8 @@ mod test {
         let mut cx = VimTestContext::new(cx, true).await;
 
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<VimSettings>(cx, |s| {
-                s.use_system_clipboard = Some(UseSystemClipboard::Never)
+            store.update_user_settings(cx, |s| {
+                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
             });
         });
 
@@ -444,8 +440,9 @@ mod test {
         let mut cx = VimTestContext::new(cx, true).await;
 
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<VimSettings>(cx, |s| {
-                s.use_system_clipboard = Some(UseSystemClipboard::OnYank)
+            store.update_user_settings(cx, |s| {
+                s.vim.get_or_insert_default().use_system_clipboard =
+                    Some(UseSystemClipboard::OnYank)
             });
         });
 
@@ -709,9 +706,9 @@ mod test {
             Mode::Normal,
         );
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                settings.languages.0.insert(
-                    LanguageName::new("Rust"),
+            store.update_user_settings(cx, |settings| {
+                settings.project.all_languages.languages.0.insert(
+                    LanguageName::new("Rust").0,
                     LanguageSettingsContent {
                         auto_indent_on_paste: Some(false),
                         ..Default::default()
@@ -772,8 +769,8 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<VimSettings>(cx, |s| {
-                s.use_system_clipboard = Some(UseSystemClipboard::Never)
+            store.update_user_settings(cx, |s| {
+                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
             });
         });
 
@@ -818,8 +815,8 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<VimSettings>(cx, |s| {
-                s.use_system_clipboard = Some(UseSystemClipboard::Never)
+            store.update_user_settings(cx, |s| {
+                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
             });
         });
 
@@ -847,8 +844,8 @@ mod test {
         let mut cx = NeovimBackedTestContext::new(cx).await;
 
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<VimSettings>(cx, |s| {
-                s.use_system_clipboard = Some(UseSystemClipboard::Never)
+            store.update_user_settings(cx, |s| {
+                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
             });
         });
 
@@ -906,8 +903,8 @@ mod test {
         let mut cx = VimTestContext::new(cx, true).await;
 
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<VimSettings>(cx, |s| {
-                s.use_system_clipboard = Some(UseSystemClipboard::Never)
+            store.update_user_settings(cx, |s| {
+                s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never)
             });
         });
 

crates/vim/src/normal/scroll.rs πŸ”—

@@ -271,7 +271,7 @@ mod test {
         state::Mode,
         test::{NeovimBackedTestContext, VimTestContext},
     };
-    use editor::{EditorSettings, ScrollBeyondLastLine};
+    use editor::ScrollBeyondLastLine;
     use gpui::{AppContext as _, point, px, size};
     use indoc::indoc;
     use language::Point;
@@ -427,9 +427,7 @@ mod test {
         // First test without vertical scroll margin
         cx.neovim.set_option(&format!("scrolloff={}", 0)).await;
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<EditorSettings>(cx, |s| {
-                s.vertical_scroll_margin = Some(0.0)
-            });
+            store.update_user_settings(cx, |s| s.editor.vertical_scroll_margin = Some(0.0));
         });
 
         let content = "Λ‡".to_owned() + &sample_text(26, 2, 'a');
@@ -455,9 +453,7 @@ mod test {
 
         cx.neovim.set_option(&format!("scrolloff={}", 3)).await;
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<EditorSettings>(cx, |s| {
-                s.vertical_scroll_margin = Some(3.0)
-            });
+            store.update_user_settings(cx, |s| s.editor.vertical_scroll_margin = Some(3.0));
         });
 
         // scroll down: ctrl-f
@@ -485,9 +481,8 @@ mod test {
         cx.set_shared_state(&content).await;
 
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<EditorSettings>(cx, |s| {
-                s.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off);
-                // s.vertical_scroll_margin = Some(0.);
+            store.update_user_settings(cx, |s| {
+                s.editor.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off);
             });
         });
 

crates/vim/src/normal/search.rs πŸ”—

@@ -645,7 +645,6 @@ mod test {
         state::Mode,
         test::{NeovimBackedTestContext, VimTestContext},
     };
-    use editor::EditorSettings;
     use editor::{DisplayPoint, display_map::DisplayRow};
 
     use indoc::indoc;
@@ -694,7 +693,7 @@ mod test {
         let mut cx = VimTestContext::new(cx, true).await;
 
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
+            store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
         });
 
         cx.set_state("Λ‡hi\nhigh\nhi\n", Mode::Normal);
@@ -815,7 +814,7 @@ mod test {
 
         // check that searching with unable search wrap
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<EditorSettings>(cx, |s| s.search_wrap = Some(false));
+            store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
         });
         cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
         cx.simulate_keystrokes("/ c c enter");

crates/vim/src/test.rs πŸ”—

@@ -22,7 +22,6 @@ pub use vim_test_context::*;
 
 use indoc::indoc;
 use search::BufferSearchBar;
-use workspace::WorkspaceSettings;
 
 use crate::{PushSneak, PushSneakBackward, insert::NormalBefore, motion, state::Mode};
 
@@ -1562,10 +1561,10 @@ async fn test_plus_minus(cx: &mut gpui::TestAppContext) {
 async fn test_command_alias(cx: &mut gpui::TestAppContext) {
     let mut cx = VimTestContext::new(cx, true).await;
     cx.update_global(|store: &mut SettingsStore, cx| {
-        store.update_user_settings::<WorkspaceSettings>(cx, |s| {
+        store.update_user_settings(cx, |s| {
             let mut aliases = HashMap::default();
             aliases.insert("Q".to_string(), "upper".to_string());
-            s.command_aliases = Some(aliases)
+            s.workspace.command_aliases = aliases
         });
     });
 

crates/vim/src/test/neovim_backed_test_context.rs πŸ”—

@@ -6,7 +6,7 @@ use std::{
     panic, thread,
 };
 
-use language::language_settings::{AllLanguageSettings, SoftWrap};
+use language::language_settings::SoftWrap;
 use util::test::marked_text_offsets;
 
 use super::{VimTestContext, neovim_connection::NeovimConnection};
@@ -245,9 +245,14 @@ impl NeovimBackedTestContext {
 
         self.update(|_, cx| {
             SettingsStore::update_global(cx, |settings, cx| {
-                settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
-                    settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength);
-                    settings.defaults.preferred_line_length = Some(columns);
+                settings.update_user_settings(cx, |settings| {
+                    settings.project.all_languages.defaults.soft_wrap =
+                        Some(SoftWrap::PreferredLineLength);
+                    settings
+                        .project
+                        .all_languages
+                        .defaults
+                        .preferred_line_length = Some(columns);
                 });
             })
         })

crates/vim/src/test/vim_test_context.rs πŸ”—

@@ -68,7 +68,7 @@ impl VimTestContext {
 
     pub fn init_keybindings(enabled: bool, cx: &mut App) {
         SettingsStore::update_global(cx, |store, cx| {
-            store.update_user_settings::<VimModeSetting>(cx, |s| s.vim_mode = Some(enabled));
+            store.update_user_settings(cx, |s| s.vim_mode = Some(enabled));
         });
         let mut default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
             "keymaps/default-macos.json",
@@ -137,7 +137,7 @@ impl VimTestContext {
     pub fn enable_vim(&mut self) {
         self.cx.update(|_, cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<VimModeSetting>(cx, |s| s.vim_mode = Some(true));
+                store.update_user_settings(cx, |s| s.vim_mode = Some(true));
             });
         })
     }
@@ -145,7 +145,7 @@ impl VimTestContext {
     pub fn disable_vim(&mut self) {
         self.cx.update(|_, cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<VimModeSetting>(cx, |s| s.vim_mode = Some(false));
+                store.update_user_settings(cx, |s| s.vim_mode = Some(false));
             });
         })
     }
@@ -153,9 +153,7 @@ impl VimTestContext {
     pub fn enable_helix(&mut self) {
         self.cx.update(|_, cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<vim_mode_setting::HelixModeSetting>(cx, |s| {
-                    s.helix_mode = Some(true)
-                });
+                store.update_user_settings(cx, |s| s.helix_mode = Some(true));
             });
         })
     }

crates/vim/src/vim.rs πŸ”—

@@ -19,7 +19,6 @@ mod state;
 mod surrounds;
 mod visual;
 
-use anyhow::Result;
 use collections::HashMap;
 use editor::{
     Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, SelectionEffects,
@@ -38,15 +37,15 @@ use normal::search::SearchSubmit;
 use object::Object;
 use schemars::JsonSchema;
 use serde::Deserialize;
-use serde::Serialize;
-use settings::{
-    Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi, update_settings_file,
+pub use settings::{
+    ModeContent, Settings, SettingsStore, UseSystemClipboard, update_settings_file,
 };
 use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals};
 use std::{mem, ops::Range, sync::Arc};
 use surrounds::SurroundsType;
 use theme::ThemeSettings;
 use ui::{IntoElement, SharedString, px};
+use util::MergeFrom;
 use vim_mode_setting::HelixModeSetting;
 use vim_mode_setting::VimModeSetting;
 use workspace::{self, Pane, Workspace};
@@ -266,7 +265,7 @@ pub fn init(cx: &mut App) {
         workspace.register_action(|workspace, _: &ToggleVimMode, _, cx| {
             let fs = workspace.app_state().fs.clone();
             let currently_enabled = Vim::enabled(cx);
-            update_settings_file::<VimModeSetting>(fs, cx, move |setting, _| {
+            update_settings_file(fs, cx, move |setting, _| {
                 setting.vim_mode = Some(!currently_enabled)
             })
         });
@@ -1793,21 +1792,19 @@ impl Vim {
     }
 }
 
-/// Controls when to use system clipboard.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum UseSystemClipboard {
-    /// Don't use system clipboard.
-    Never,
-    /// Use system clipboard.
-    Always,
-    /// Use system clipboard for yank operations.
-    OnYank,
+struct VimSettings {
+    pub default_mode: Mode,
+    pub toggle_relative_line_numbers: bool,
+    pub use_system_clipboard: settings::UseSystemClipboard,
+    pub use_smartcase_find: bool,
+    pub custom_digraphs: HashMap<String, Arc<str>>,
+    pub highlight_on_yank_duration: u64,
+    pub cursor_shape: CursorShapeSettings,
 }
 
 /// The settings for cursor shape.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-struct CursorShapeSettings {
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct CursorShapeSettings {
     /// Cursor shape for the normal mode.
     ///
     /// Default: block
@@ -1826,85 +1823,63 @@ struct CursorShapeSettings {
     pub insert: Option<CursorShape>,
 }
 
-#[derive(Deserialize)]
-struct VimSettings {
-    pub default_mode: Mode,
-    pub toggle_relative_line_numbers: bool,
-    pub use_system_clipboard: UseSystemClipboard,
-    pub use_smartcase_find: bool,
-    pub custom_digraphs: HashMap<String, Arc<str>>,
-    pub highlight_on_yank_duration: u64,
-    pub cursor_shape: CursorShapeSettings,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(key = "vim")]
-struct VimSettingsContent {
-    pub default_mode: Option<ModeContent>,
-    pub toggle_relative_line_numbers: Option<bool>,
-    pub use_system_clipboard: Option<UseSystemClipboard>,
-    pub use_smartcase_find: Option<bool>,
-    pub custom_digraphs: Option<HashMap<String, Arc<str>>>,
-    pub highlight_on_yank_duration: Option<u64>,
-    pub cursor_shape: Option<CursorShapeSettings>,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ModeContent {
-    #[default]
-    Normal,
-    Insert,
-    Replace,
-    Visual,
-    VisualLine,
-    VisualBlock,
-    HelixNormal,
+impl From<settings::CursorShapeSettings> for CursorShapeSettings {
+    fn from(settings: settings::CursorShapeSettings) -> Self {
+        Self {
+            normal: settings.normal.map(Into::into),
+            replace: settings.replace.map(Into::into),
+            visual: settings.visual.map(Into::into),
+            insert: settings.insert.map(Into::into),
+        }
+    }
 }
 
-impl From<ModeContent> for Mode {
+impl From<settings::ModeContent> for Mode {
     fn from(mode: ModeContent) -> Self {
         match mode {
             ModeContent::Normal => Self::Normal,
             ModeContent::Insert => Self::Insert,
-            ModeContent::Replace => Self::Replace,
-            ModeContent::Visual => Self::Visual,
-            ModeContent::VisualLine => Self::VisualLine,
-            ModeContent::VisualBlock => Self::VisualBlock,
             ModeContent::HelixNormal => Self::HelixNormal,
         }
     }
 }
 
 impl Settings for VimSettings {
-    type FileContent = VimSettingsContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        let settings: VimSettingsContent = sources.json_merge()?;
-
-        Ok(Self {
-            default_mode: settings
-                .default_mode
-                .ok_or_else(Self::missing_default)?
-                .into(),
-            toggle_relative_line_numbers: settings
-                .toggle_relative_line_numbers
-                .ok_or_else(Self::missing_default)?,
-            use_system_clipboard: settings
-                .use_system_clipboard
-                .ok_or_else(Self::missing_default)?,
-            use_smartcase_find: settings
-                .use_smartcase_find
-                .ok_or_else(Self::missing_default)?,
-            custom_digraphs: settings.custom_digraphs.ok_or_else(Self::missing_default)?,
-            highlight_on_yank_duration: settings
-                .highlight_on_yank_duration
-                .ok_or_else(Self::missing_default)?,
-            cursor_shape: settings.cursor_shape.ok_or_else(Self::missing_default)?,
-        })
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let vim = content.vim.clone().unwrap();
+        Self {
+            default_mode: vim.default_mode.unwrap().into(),
+            toggle_relative_line_numbers: vim.toggle_relative_line_numbers.unwrap(),
+            use_system_clipboard: vim.use_system_clipboard.unwrap(),
+            use_smartcase_find: vim.use_smartcase_find.unwrap(),
+            custom_digraphs: vim.custom_digraphs.unwrap(),
+            highlight_on_yank_duration: vim.highlight_on_yank_duration.unwrap(),
+            cursor_shape: vim.cursor_shape.unwrap().into(),
+        }
+    }
+
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(vim) = content.vim.as_ref() else {
+            return;
+        };
+        self.default_mode
+            .merge_from(&vim.default_mode.map(Into::into));
+        self.toggle_relative_line_numbers
+            .merge_from(&vim.toggle_relative_line_numbers);
+        self.use_system_clipboard
+            .merge_from(&vim.use_system_clipboard);
+        self.use_smartcase_find.merge_from(&vim.use_smartcase_find);
+        self.custom_digraphs.merge_from(&vim.custom_digraphs);
+        self.highlight_on_yank_duration
+            .merge_from(&vim.highlight_on_yank_duration);
+        self.cursor_shape
+            .merge_from(&vim.cursor_shape.map(Into::into));
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {
+    fn import_from_vscode(
+        _vscode: &settings::VsCodeSettings,
+        _current: &mut settings::SettingsContent,
+    ) {
         // TODO: translate vim extension settings
     }
 }

crates/vim_mode_setting/Cargo.toml πŸ”—

@@ -12,9 +12,6 @@ workspace = true
 path = "src/vim_mode_setting.rs"
 
 [dependencies]
-anyhow.workspace = true
 gpui.workspace = true
-schemars.workspace = true
-serde.workspace = true
 settings.workspace = true
 workspace-hack.workspace = true

crates/vim_mode_setting/src/vim_mode_setting.rs πŸ”—

@@ -4,10 +4,8 @@
 //! disable Vim/Helix modes without having to depend on the `vim` crate in its
 //! entirety.
 
-use anyhow::Result;
 use gpui::App;
-use schemars::JsonSchema;
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsContent};
 
 /// Initializes the `vim_mode_setting` crate.
 pub fn init(cx: &mut App) {
@@ -17,97 +15,34 @@ pub fn init(cx: &mut App) {
 
 pub struct VimModeSetting(pub bool);
 
-#[derive(
-    Copy,
-    Clone,
-    PartialEq,
-    Eq,
-    Debug,
-    Default,
-    serde::Serialize,
-    serde::Deserialize,
-    SettingsUi,
-    SettingsKey,
-    JsonSchema,
-)]
-#[settings_key(None)]
-pub struct VimModeSettingContent {
-    /// Whether or not to enable Vim mode.
-    ///
-    /// Default: false
-    pub vim_mode: Option<bool>,
-}
-
 impl Settings for VimModeSetting {
-    type FileContent = VimModeSettingContent;
+    fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self {
+        Self(content.vim_mode.unwrap())
+    }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        Ok(Self(
-            [
-                sources.profile,
-                sources.release_channel,
-                sources.user,
-                sources.server,
-                Some(sources.default),
-            ]
-            .into_iter()
-            .flatten()
-            .filter_map(|mode| mode.vim_mode)
-            .next()
-            .ok_or_else(Self::missing_default)?,
-        ))
+    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
+        if let Some(vim_mode) = content.vim_mode {
+            self.0 = vim_mode;
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {
+    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _content: &mut SettingsContent) {
         // TODO: could possibly check if any of the `vim.<foo>` keys are set?
     }
 }
 
-#[derive(Debug)]
 pub struct HelixModeSetting(pub bool);
 
-#[derive(
-    Copy,
-    Clone,
-    PartialEq,
-    Eq,
-    Debug,
-    Default,
-    serde::Serialize,
-    serde::Deserialize,
-    SettingsUi,
-    SettingsKey,
-    JsonSchema,
-)]
-#[settings_key(None)]
-pub struct HelixModeSettingContent {
-    /// Whether or not to enable Helix mode.
-    ///
-    /// Default: false
-    pub helix_mode: Option<bool>,
-}
-
 impl Settings for HelixModeSetting {
-    type FileContent = HelixModeSettingContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        Ok(Self(
-            [
-                sources.profile,
-                sources.release_channel,
-                sources.user,
-                sources.server,
-                Some(sources.default),
-            ]
-            .into_iter()
-            .flatten()
-            .filter_map(|mode| mode.helix_mode)
-            .next()
-            .ok_or_else(Self::missing_default)?,
-        ))
+    fn from_defaults(content: &SettingsContent, _cx: &mut App) -> Self {
+        Self(content.helix_mode.unwrap())
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {
-        // TODO: could possibly check if any of the `helix.<foo>` keys are set?
+    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
+        if let Some(helix_mode) = content.helix_mode {
+            self.0 = helix_mode;
+        }
     }
+
+    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {}
 }

crates/workspace/src/dock.rs πŸ”—

@@ -9,8 +9,6 @@ use gpui::{
     Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div,
     px,
 };
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
 use settings::SettingsStore;
 use std::sync::Arc;
 use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex};
@@ -210,14 +208,33 @@ impl Focusable for Dock {
     }
 }
 
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
-#[serde(rename_all = "lowercase")]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum DockPosition {
     Left,
     Bottom,
     Right,
 }
 
+impl From<settings::DockPosition> for DockPosition {
+    fn from(value: settings::DockPosition) -> Self {
+        match value {
+            settings::DockPosition::Left => Self::Left,
+            settings::DockPosition::Bottom => Self::Bottom,
+            settings::DockPosition::Right => Self::Right,
+        }
+    }
+}
+
+impl Into<settings::DockPosition> for DockPosition {
+    fn into(self) -> settings::DockPosition {
+        match self {
+            Self::Left => settings::DockPosition::Left,
+            Self::Bottom => settings::DockPosition::Bottom,
+            Self::Right => settings::DockPosition::Right,
+        }
+    }
+}
+
 impl DockPosition {
     fn label(&self) -> &'static str {
         match self {

crates/workspace/src/item.rs πŸ”—

@@ -15,9 +15,9 @@ use gpui::{
     Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task, WeakEntity, Window,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsLocation, SettingsSources, SettingsUi};
+pub use settings::{
+    ActivateOnClose, ClosePosition, Settings, SettingsLocation, ShowCloseButton, ShowDiagnostics,
+};
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -30,7 +30,7 @@ use std::{
 };
 use theme::Theme;
 use ui::{Color, Icon, IntoElement, Label, LabelCommon};
-use util::ResultExt;
+use util::{MergeFrom as _, ResultExt};
 
 pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
 
@@ -49,7 +49,6 @@ impl Default for SaveOptions {
     }
 }
 
-#[derive(Deserialize)]
 pub struct ItemSettings {
     pub git_status: bool,
     pub close_position: ClosePosition,
@@ -59,150 +58,119 @@ pub struct ItemSettings {
     pub show_close_button: ShowCloseButton,
 }
 
-#[derive(Deserialize)]
 pub struct PreviewTabsSettings {
     pub enabled: bool,
     pub enable_preview_from_file_finder: bool,
     pub enable_preview_from_code_navigation: bool,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "lowercase")]
-pub enum ClosePosition {
-    Left,
-    #[default]
-    Right,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "lowercase")]
-pub enum ShowCloseButton {
-    Always,
-    #[default]
-    Hover,
-    Hidden,
-}
-
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowDiagnostics {
-    #[default]
-    Off,
-    Errors,
-    All,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ActivateOnClose {
-    #[default]
-    History,
-    Neighbour,
-    LeftNeighbour,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(key = "tabs")]
-pub struct ItemSettingsContent {
-    /// Whether to show the Git file status on a tab item.
-    ///
-    /// Default: false
-    git_status: Option<bool>,
-    /// Position of the close button in a tab.
-    ///
-    /// Default: right
-    close_position: Option<ClosePosition>,
-    /// Whether to show the file icon for a tab.
-    ///
-    /// Default: false
-    file_icons: Option<bool>,
-    /// What to do after closing the current tab.
-    ///
-    /// Default: history
-    pub activate_on_close: Option<ActivateOnClose>,
-    /// Which files containing diagnostic errors/warnings to mark in the tabs.
-    /// This setting can take the following three values:
-    ///
-    /// Default: off
-    show_diagnostics: Option<ShowDiagnostics>,
-    /// Whether to always show the close button on tabs.
-    ///
-    /// Default: false
-    show_close_button: Option<ShowCloseButton>,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(key = "preview_tabs")]
-pub struct PreviewTabsSettingsContent {
-    /// Whether to show opened editors as preview tabs.
-    /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic.
-    ///
-    /// Default: true
-    enabled: Option<bool>,
-    /// Whether to open tabs in preview mode when selected from the file finder.
-    ///
-    /// Default: false
-    enable_preview_from_file_finder: Option<bool>,
-    /// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
-    ///
-    /// Default: false
-    enable_preview_from_code_navigation: Option<bool>,
-}
-
 impl Settings for ItemSettings {
-    type FileContent = ItemSettingsContent;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let tabs = content.tabs.as_ref().unwrap();
+        Self {
+            git_status: tabs.git_status.unwrap(),
+            close_position: tabs.close_position.unwrap(),
+            activate_on_close: tabs.activate_on_close.unwrap(),
+            file_icons: tabs.file_icons.unwrap(),
+            show_diagnostics: tabs.show_diagnostics.unwrap(),
+            show_close_button: tabs.show_close_button.unwrap(),
+        }
+    }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(tabs) = content.tabs.as_ref() else {
+            return;
+        };
+        self.git_status.merge_from(&tabs.git_status);
+        self.close_position.merge_from(&tabs.close_position);
+        self.activate_on_close.merge_from(&tabs.activate_on_close);
+        self.file_icons.merge_from(&tabs.file_icons);
+        self.show_diagnostics.merge_from(&tabs.show_diagnostics);
+        self.show_close_button.merge_from(&tabs.show_close_button);
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
+    fn import_from_vscode(
+        vscode: &settings::VsCodeSettings,
+        current: &mut settings::SettingsContent,
+    ) {
         if let Some(b) = vscode.read_bool("workbench.editor.tabActionCloseVisibility") {
-            current.show_close_button = Some(if b {
+            current.tabs.get_or_insert_default().show_close_button = Some(if b {
                 ShowCloseButton::Always
             } else {
                 ShowCloseButton::Hidden
             })
         }
-        vscode.enum_setting(
-            "workbench.editor.tabActionLocation",
-            &mut current.close_position,
-            |s| match s {
-                "right" => Some(ClosePosition::Right),
-                "left" => Some(ClosePosition::Left),
-                _ => None,
-            },
-        );
+        if let Some(s) = vscode.read_enum("workbench.editor.tabActionLocation", |s| match s {
+            "right" => Some(ClosePosition::Right),
+            "left" => Some(ClosePosition::Left),
+            _ => None,
+        }) {
+            current.tabs.get_or_insert_default().close_position = Some(s)
+        }
         if let Some(b) = vscode.read_bool("workbench.editor.focusRecentEditorAfterClose") {
-            current.activate_on_close = Some(if b {
+            current.tabs.get_or_insert_default().activate_on_close = Some(if b {
                 ActivateOnClose::History
             } else {
                 ActivateOnClose::LeftNeighbour
             })
         }
 
-        vscode.bool_setting("workbench.editor.showIcons", &mut current.file_icons);
-        vscode.bool_setting("git.decorations.enabled", &mut current.git_status);
+        if let Some(b) = vscode.read_bool("workbench.editor.showIcons") {
+            current.tabs.get_or_insert_default().file_icons = Some(b);
+        };
+        if let Some(b) = vscode.read_bool("git.decorations.enabled") {
+            current.tabs.get_or_insert_default().git_status = Some(b);
+        }
     }
 }
 
 impl Settings for PreviewTabsSettings {
-    type FileContent = PreviewTabsSettingsContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
-    }
-
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
-        vscode.bool_setting("workbench.editor.enablePreview", &mut current.enabled);
-        vscode.bool_setting(
-            "workbench.editor.enablePreviewFromCodeNavigation",
-            &mut current.enable_preview_from_code_navigation,
-        );
-        vscode.bool_setting(
-            "workbench.editor.enablePreviewFromQuickOpen",
-            &mut current.enable_preview_from_file_finder,
-        );
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let preview_tabs = content.preview_tabs.as_ref().unwrap();
+        Self {
+            enabled: preview_tabs.enabled.unwrap(),
+            enable_preview_from_file_finder: preview_tabs.enable_preview_from_file_finder.unwrap(),
+            enable_preview_from_code_navigation: preview_tabs
+                .enable_preview_from_code_navigation
+                .unwrap(),
+        }
+    }
+
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(preview_tabs) = content.preview_tabs.as_ref() else {
+            return;
+        };
+
+        self.enabled.merge_from(&preview_tabs.enabled);
+        self.enable_preview_from_file_finder
+            .merge_from(&preview_tabs.enable_preview_from_file_finder);
+        self.enable_preview_from_code_navigation
+            .merge_from(&preview_tabs.enable_preview_from_code_navigation);
+    }
+
+    fn import_from_vscode(
+        vscode: &settings::VsCodeSettings,
+        current: &mut settings::SettingsContent,
+    ) {
+        if let Some(enabled) = vscode.read_bool("workbench.editor.enablePreview") {
+            current.preview_tabs.get_or_insert_default().enabled = Some(enabled);
+        }
+        if let Some(enable_preview_from_code_navigation) =
+            vscode.read_bool("workbench.editor.enablePreviewFromCodeNavigation")
+        {
+            current
+                .preview_tabs
+                .get_or_insert_default()
+                .enable_preview_from_code_navigation = Some(enable_preview_from_code_navigation)
+        }
+        if let Some(enable_preview_from_file_finder) =
+            vscode.read_bool("workbench.editor.enablePreviewFromQuickOpen")
+        {
+            current
+                .preview_tabs
+                .get_or_insert_default()
+                .enable_preview_from_file_finder = Some(enable_preview_from_file_finder)
+        }
     }
 }
 

crates/workspace/src/pane.rs πŸ”—

@@ -5816,8 +5816,8 @@ mod tests {
     async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) {
         init_test(cx);
         cx.update_global::<SettingsStore, ()>(|s, cx| {
-            s.update_user_settings::<ItemSettings>(cx, |s| {
-                s.activate_on_close = Some(ActivateOnClose::Neighbour);
+            s.update_user_settings(cx, |s| {
+                s.tabs.get_or_insert_default().activate_on_close = Some(ActivateOnClose::Neighbour);
             });
         });
         let fs = FakeFs::new(cx.executor());
@@ -5905,8 +5905,9 @@ mod tests {
     async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) {
         init_test(cx);
         cx.update_global::<SettingsStore, ()>(|s, cx| {
-            s.update_user_settings::<ItemSettings>(cx, |s| {
-                s.activate_on_close = Some(ActivateOnClose::LeftNeighbour);
+            s.update_user_settings(cx, |s| {
+                s.tabs.get_or_insert_default().activate_on_close =
+                    Some(ActivateOnClose::LeftNeighbour);
             });
         });
         let fs = FakeFs::new(cx.executor());
@@ -6558,8 +6559,8 @@ mod tests {
 
     fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-                settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
+            store.update_user_settings(cx, |settings| {
+                settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap())
             });
         });
     }

crates/workspace/src/workspace.rs πŸ”—

@@ -52,10 +52,7 @@ pub use item::{
     ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
 };
 use itertools::Itertools;
-use language::{
-    Buffer, LanguageRegistry, Rope,
-    language_settings::{AllLanguageSettings, all_language_settings},
-};
+use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings};
 pub use modal_layer::*;
 use node_runtime::NodeRuntime;
 use notifications::{
@@ -1695,8 +1692,8 @@ impl Workspace {
         cx: &mut Context<Self>,
     ) {
         let fs = self.project().read(cx).fs();
-        settings::update_settings_file::<WorkspaceSettings>(fs.clone(), cx, move |content, _cx| {
-            content.bottom_dock_layout = Some(layout);
+        settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
+            content.workspace.bottom_dock_layout = Some(layout);
         });
 
         cx.notify();
@@ -6014,8 +6011,8 @@ impl Workspace {
     ) {
         let fs = self.project().read(cx).fs().clone();
         let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
-        update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
-            file.defaults.show_edit_predictions = Some(!show_edit_predictions)
+        update_settings_file(fs, cx, move |file, _| {
+            file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
         });
     }
 }
@@ -8678,8 +8675,8 @@ mod tests {
         // Autosave on window change.
         item.update(cx, |item, cx| {
             SettingsStore::update_global(cx, |settings, cx| {
-                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
+                settings.update_user_settings(cx, |settings| {
+                    settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
                 })
             });
             item.is_dirty = true;
@@ -8698,13 +8695,12 @@ mod tests {
         item.update_in(cx, |item, window, cx| {
             cx.focus_self(window);
             SettingsStore::update_global(cx, |settings, cx| {
-                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
+                settings.update_user_settings(cx, |settings| {
+                    settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
                 })
             });
             item.is_dirty = true;
         });
-
         // Blurring the item saves the file.
         item.update_in(cx, |_, window, _| window.blur());
         cx.executor().run_until_parked();
@@ -8721,8 +8717,9 @@ mod tests {
         // Autosave after delay.
         item.update(cx, |item, cx| {
             SettingsStore::update_global(cx, |settings, cx| {
-                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
+                settings.update_user_settings(cx, |settings| {
+                    settings.workspace.autosave =
+                        Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
                 })
             });
             item.is_dirty = true;
@@ -8770,8 +8767,8 @@ mod tests {
         // Autosave on focus change, ensuring closing the tab counts as such.
         item.update(cx, |item, cx| {
             SettingsStore::update_global(cx, |settings, cx| {
-                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
+                settings.update_user_settings(cx, |settings| {
+                    settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
                 })
             });
             item.is_dirty = true;
@@ -9774,8 +9771,8 @@ mod tests {
 
         // Enable the close_on_disk_deletion setting
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-                settings.close_on_file_delete = Some(true);
+            store.update_user_settings(cx, |settings| {
+                settings.workspace.close_on_file_delete = Some(true);
             });
         });
 
@@ -9842,8 +9839,8 @@ mod tests {
 
         // Ensure close_on_disk_deletion is disabled (default)
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-                settings.close_on_file_delete = Some(false);
+            store.update_user_settings(cx, |settings| {
+                settings.workspace.close_on_file_delete = Some(false);
             });
         });
 
@@ -9919,8 +9916,8 @@ mod tests {
 
         // Enable the close_on_file_delete setting
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-                settings.close_on_file_delete = Some(true);
+            store.update_user_settings(cx, |settings| {
+                settings.workspace.close_on_file_delete = Some(true);
             });
         });
 
@@ -9992,8 +9989,8 @@ mod tests {
 
         // Enable the close_on_file_delete setting
         cx.update_global(|store: &mut SettingsStore, cx| {
-            store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
-                settings.close_on_file_delete = Some(true);
+            store.update_user_settings(cx, |settings| {
+                settings.workspace.close_on_file_delete = Some(true);
             });
         });
 

crates/workspace/src/workspace_settings.rs πŸ”—

@@ -1,65 +1,49 @@
 use std::num::NonZeroUsize;
 
 use crate::DockPosition;
-use anyhow::Result;
 use collections::HashMap;
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use serde::Deserialize;
+pub use settings::AutosaveSetting;
+use settings::Settings;
+pub use settings::{
+    BottomDockLayout, PaneSplitDirectionHorizontal, PaneSplitDirectionVertical,
+    RestoreOnStartupBehavior,
+};
+use util::MergeFrom as _;
 
-#[derive(Deserialize)]
 pub struct WorkspaceSettings {
     pub active_pane_modifiers: ActivePanelModifiers,
-    pub bottom_dock_layout: BottomDockLayout,
-    pub pane_split_direction_horizontal: PaneSplitDirectionHorizontal,
-    pub pane_split_direction_vertical: PaneSplitDirectionVertical,
-    pub centered_layout: CenteredLayoutSettings,
+    pub bottom_dock_layout: settings::BottomDockLayout,
+    pub pane_split_direction_horizontal: settings::PaneSplitDirectionHorizontal,
+    pub pane_split_direction_vertical: settings::PaneSplitDirectionVertical,
+    pub centered_layout: settings::CenteredLayoutSettings,
     pub confirm_quit: bool,
     pub show_call_status_icon: bool,
     pub autosave: AutosaveSetting,
-    pub restore_on_startup: RestoreOnStartupBehavior,
+    pub restore_on_startup: settings::RestoreOnStartupBehavior,
     pub restore_on_file_reopen: bool,
     pub drop_target_size: f32,
     pub use_system_path_prompts: bool,
     pub use_system_prompts: bool,
     pub command_aliases: HashMap<String, String>,
     pub max_tabs: Option<NonZeroUsize>,
-    pub when_closing_with_no_tabs: CloseWindowWhenNoItems,
-    pub on_last_window_closed: OnLastWindowClosed,
+    pub when_closing_with_no_tabs: settings::CloseWindowWhenNoItems,
+    pub on_last_window_closed: settings::OnLastWindowClosed,
     pub resize_all_panels_in_dock: Vec<DockPosition>,
     pub close_on_file_delete: bool,
     pub use_system_window_tabs: bool,
     pub zoomed_padding: bool,
 }
 
-#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum OnLastWindowClosed {
-    /// Match platform conventions by default, so don't quit on macOS, and quit on other platforms
-    #[default]
-    PlatformDefault,
-    /// Quit the application the last window is closed
-    QuitApp,
-}
-
-impl OnLastWindowClosed {
-    pub fn is_quit_app(&self) -> bool {
-        match self {
-            OnLastWindowClosed::PlatformDefault => false,
-            OnLastWindowClosed::QuitApp => true,
-        }
-    }
-}
-
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
+#[derive(Copy, Clone, PartialEq, Debug, Default)]
 pub struct ActivePanelModifiers {
     /// Size of the border surrounding the active pane.
     /// When set to 0, the active pane doesn't have any border.
     /// The border is drawn inset.
     ///
     /// Default: `0.0`
+    // TODO: make this not an option, it is never None
     pub border_size: Option<f32>,
     /// Opacity of inactive panels.
     /// When set to 1.0, the inactive panes have the same opacity as the active one.
@@ -67,156 +51,10 @@ pub struct ActivePanelModifiers {
     /// Values are clamped to the [0.0, 1.0] range.
     ///
     /// Default: `1.0`
+    // TODO: make this not an option, it is never None
     pub inactive_opacity: Option<f32>,
 }
 
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum BottomDockLayout {
-    /// Contained between the left and right docks
-    #[default]
-    Contained,
-    /// Takes up the full width of the window
-    Full,
-    /// Extends under the left dock while snapping to the right dock
-    LeftAligned,
-    /// Extends under the right dock while snapping to the left dock
-    RightAligned,
-}
-
-#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum CloseWindowWhenNoItems {
-    /// Match platform conventions by default, so "on" on macOS and "off" everywhere else
-    #[default]
-    PlatformDefault,
-    /// Close the window when there are no tabs
-    CloseWindow,
-    /// Leave the window open when there are no tabs
-    KeepWindowOpen,
-}
-
-impl CloseWindowWhenNoItems {
-    pub fn should_close(&self) -> bool {
-        match self {
-            CloseWindowWhenNoItems::PlatformDefault => cfg!(target_os = "macos"),
-            CloseWindowWhenNoItems::CloseWindow => true,
-            CloseWindowWhenNoItems::KeepWindowOpen => false,
-        }
-    }
-}
-
-#[derive(Copy, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum RestoreOnStartupBehavior {
-    /// Always start with an empty editor
-    None,
-    /// Restore the workspace that was closed last.
-    LastWorkspace,
-    /// Restore all workspaces that were open when quitting Zed.
-    #[default]
-    LastSession,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
-pub struct WorkspaceSettingsContent {
-    /// Active pane styling settings.
-    pub active_pane_modifiers: Option<ActivePanelModifiers>,
-    /// Layout mode for the bottom dock
-    ///
-    /// Default: contained
-    pub bottom_dock_layout: Option<BottomDockLayout>,
-    /// Direction to split horizontally.
-    ///
-    /// Default: "up"
-    pub pane_split_direction_horizontal: Option<PaneSplitDirectionHorizontal>,
-    /// Direction to split vertically.
-    ///
-    /// Default: "left"
-    pub pane_split_direction_vertical: Option<PaneSplitDirectionVertical>,
-    /// Centered layout related settings.
-    pub centered_layout: Option<CenteredLayoutSettings>,
-    /// Whether or not to prompt the user to confirm before closing the application.
-    ///
-    /// Default: false
-    pub confirm_quit: Option<bool>,
-    /// Whether or not to show the call status icon in the status bar.
-    ///
-    /// Default: true
-    pub show_call_status_icon: Option<bool>,
-    /// When to automatically save edited buffers.
-    ///
-    /// Default: off
-    pub autosave: Option<AutosaveSetting>,
-    /// Controls previous session restoration in freshly launched Zed instance.
-    /// Values: none, last_workspace, last_session
-    /// Default: last_session
-    pub restore_on_startup: Option<RestoreOnStartupBehavior>,
-    /// Whether to attempt to restore previous file's state when opening it again.
-    /// The state is stored per pane.
-    /// When disabled, defaults are applied instead of the state restoration.
-    ///
-    /// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane.
-    /// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default.
-    ///
-    /// Default: true
-    pub restore_on_file_reopen: Option<bool>,
-    /// The size of the workspace split drop targets on the outer edges.
-    /// Given as a fraction that will be multiplied by the smaller dimension of the workspace.
-    ///
-    /// Default: `0.2` (20% of the smaller dimension of the workspace)
-    pub drop_target_size: Option<f32>,
-    /// Whether to close the window when using 'close active item' on a workspace with no tabs
-    ///
-    /// Default: auto ("on" on macOS, "off" otherwise)
-    pub when_closing_with_no_tabs: Option<CloseWindowWhenNoItems>,
-    /// Whether to use the system provided dialogs for Open and Save As.
-    /// When set to false, Zed will use the built-in keyboard-first pickers.
-    ///
-    /// Default: true
-    pub use_system_path_prompts: Option<bool>,
-    /// Whether to use the system provided prompts.
-    /// When set to false, Zed will use the built-in prompts.
-    /// Note that this setting has no effect on Linux, where Zed will always
-    /// use the built-in prompts.
-    ///
-    /// Default: true
-    pub use_system_prompts: Option<bool>,
-    /// Aliases for the command palette. When you type a key in this map,
-    /// it will be assumed to equal the value.
-    ///
-    /// Default: true
-    pub command_aliases: Option<HashMap<String, String>>,
-    /// Maximum open tabs in a pane. Will not close an unsaved
-    /// tab. Set to `None` for unlimited tabs.
-    ///
-    /// Default: none
-    pub max_tabs: Option<NonZeroUsize>,
-    /// What to do when the last window is closed
-    ///
-    /// Default: auto (nothing on macOS, "app quit" otherwise)
-    pub on_last_window_closed: Option<OnLastWindowClosed>,
-    /// Whether to resize all the panels in a dock when resizing the dock.
-    ///
-    /// Default: ["left"]
-    pub resize_all_panels_in_dock: Option<Vec<DockPosition>>,
-    /// Whether to automatically close files that have been deleted on disk.
-    ///
-    /// Default: false
-    pub close_on_file_delete: Option<bool>,
-    /// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only).
-    ///
-    /// Default: false
-    pub use_system_window_tabs: Option<bool>,
-    /// Whether to show padding for zoomed panels.
-    /// When enabled, zoomed bottom panels will have some top padding,
-    /// while zoomed left/right panels will have padding to the right/left (respectively).
-    ///
-    /// Default: true
-    pub zoomed_padding: Option<bool>,
-}
-
 #[derive(Deserialize)]
 pub struct TabBarSettings {
     pub show: bool,
@@ -224,84 +62,118 @@ pub struct TabBarSettings {
     pub show_tab_bar_buttons: bool,
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(key = "tab_bar")]
-pub struct TabBarSettingsContent {
-    /// Whether or not to show the tab bar in the editor.
-    ///
-    /// Default: true
-    pub show: Option<bool>,
-    /// Whether or not to show the navigation history buttons in the tab bar.
-    ///
-    /// Default: true
-    pub show_nav_history_buttons: Option<bool>,
-    /// Whether or not to show the tab bar buttons.
-    ///
-    /// Default: true
-    pub show_tab_bar_buttons: Option<bool>,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum AutosaveSetting {
-    /// Disable autosave.
-    Off,
-    /// Save after inactivity period of `milliseconds`.
-    AfterDelay { milliseconds: u64 },
-    /// Autosave when focus changes.
-    OnFocusChange,
-    /// Autosave when the active window changes.
-    OnWindowChange,
-}
-
-impl AutosaveSetting {
-    pub fn should_save_on_close(&self) -> bool {
-        matches!(
-            &self,
-            AutosaveSetting::OnFocusChange
-                | AutosaveSetting::OnWindowChange
-                | AutosaveSetting::AfterDelay { .. }
-        )
+impl Settings for WorkspaceSettings {
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let workspace = &content.workspace;
+        Self {
+            active_pane_modifiers: ActivePanelModifiers {
+                border_size: Some(
+                    workspace
+                        .active_pane_modifiers
+                        .unwrap()
+                        .border_size
+                        .unwrap(),
+                ),
+                inactive_opacity: Some(
+                    workspace
+                        .active_pane_modifiers
+                        .unwrap()
+                        .inactive_opacity
+                        .unwrap(),
+                ),
+            },
+            bottom_dock_layout: workspace.bottom_dock_layout.unwrap(),
+            pane_split_direction_horizontal: workspace.pane_split_direction_horizontal.unwrap(),
+            pane_split_direction_vertical: workspace.pane_split_direction_vertical.unwrap(),
+            centered_layout: workspace.centered_layout.unwrap(),
+            confirm_quit: workspace.confirm_quit.unwrap(),
+            show_call_status_icon: workspace.show_call_status_icon.unwrap(),
+            autosave: workspace.autosave.unwrap(),
+            restore_on_startup: workspace.restore_on_startup.unwrap(),
+            restore_on_file_reopen: workspace.restore_on_file_reopen.unwrap(),
+            drop_target_size: workspace.drop_target_size.unwrap(),
+            use_system_path_prompts: workspace.use_system_path_prompts.unwrap(),
+            use_system_prompts: workspace.use_system_prompts.unwrap(),
+            command_aliases: workspace.command_aliases.clone(),
+            max_tabs: workspace.max_tabs,
+            when_closing_with_no_tabs: workspace.when_closing_with_no_tabs.unwrap(),
+            on_last_window_closed: workspace.on_last_window_closed.unwrap(),
+            resize_all_panels_in_dock: workspace
+                .resize_all_panels_in_dock
+                .clone()
+                .unwrap()
+                .into_iter()
+                .map(Into::into)
+                .collect(),
+            close_on_file_delete: workspace.close_on_file_delete.unwrap(),
+            use_system_window_tabs: workspace.use_system_window_tabs.unwrap(),
+            zoomed_padding: workspace.zoomed_padding.unwrap(),
+        }
     }
-}
 
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum PaneSplitDirectionHorizontal {
-    Up,
-    Down,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum PaneSplitDirectionVertical {
-    Left,
-    Right,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi)]
-#[serde(rename_all = "snake_case")]
-pub struct CenteredLayoutSettings {
-    /// The relative width of the left padding of the central pane from the
-    /// workspace when the centered layout is used.
-    ///
-    /// Default: 0.2
-    pub left_padding: Option<f32>,
-    // The relative width of the right padding of the central pane from the
-    // workspace when the centered layout is used.
-    ///
-    /// Default: 0.2
-    pub right_padding: Option<f32>,
-}
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let workspace = &content.workspace;
+        if let Some(border_size) = workspace
+            .active_pane_modifiers
+            .and_then(|modifier| modifier.border_size)
+        {
+            self.active_pane_modifiers.border_size = Some(border_size);
+        }
 
-impl Settings for WorkspaceSettings {
-    type FileContent = WorkspaceSettingsContent;
+        if let Some(inactive_opacity) = workspace
+            .active_pane_modifiers
+            .and_then(|modifier| modifier.inactive_opacity)
+        {
+            self.active_pane_modifiers.inactive_opacity = Some(inactive_opacity);
+        }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
+        self.bottom_dock_layout
+            .merge_from(&workspace.bottom_dock_layout);
+        self.pane_split_direction_horizontal
+            .merge_from(&workspace.pane_split_direction_horizontal);
+        self.pane_split_direction_vertical
+            .merge_from(&workspace.pane_split_direction_vertical);
+        self.centered_layout.merge_from(&workspace.centered_layout);
+        self.confirm_quit.merge_from(&workspace.confirm_quit);
+        self.show_call_status_icon
+            .merge_from(&workspace.show_call_status_icon);
+        self.autosave.merge_from(&workspace.autosave);
+        self.restore_on_startup
+            .merge_from(&workspace.restore_on_startup);
+        self.restore_on_file_reopen
+            .merge_from(&workspace.restore_on_file_reopen);
+        self.drop_target_size
+            .merge_from(&workspace.drop_target_size);
+        self.use_system_path_prompts
+            .merge_from(&workspace.use_system_path_prompts);
+        self.use_system_prompts
+            .merge_from(&workspace.use_system_prompts);
+        self.command_aliases
+            .extend(workspace.command_aliases.clone());
+        if let Some(max_tabs) = workspace.max_tabs {
+            self.max_tabs = Some(max_tabs);
+        }
+        self.when_closing_with_no_tabs
+            .merge_from(&workspace.when_closing_with_no_tabs);
+        self.on_last_window_closed
+            .merge_from(&workspace.on_last_window_closed);
+        self.resize_all_panels_in_dock.merge_from(
+            &workspace
+                .resize_all_panels_in_dock
+                .as_ref()
+                .map(|resize| resize.clone().into_iter().map(Into::into).collect()),
+        );
+        self.close_on_file_delete
+            .merge_from(&workspace.close_on_file_delete);
+        self.use_system_window_tabs
+            .merge_from(&workspace.use_system_window_tabs);
+        self.zoomed_padding.merge_from(&workspace.zoomed_padding);
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
+    fn import_from_vscode(
+        vscode: &settings::VsCodeSettings,
+        current: &mut settings::SettingsContent,
+    ) {
         if vscode
             .read_bool("accessibility.dimUnfocused.enabled")
             .unwrap_or_default()
@@ -309,19 +181,16 @@ impl Settings for WorkspaceSettings {
                 .read_value("accessibility.dimUnfocused.opacity")
                 .and_then(|v| v.as_f64())
         {
-            if let Some(settings) = current.active_pane_modifiers.as_mut() {
-                settings.inactive_opacity = Some(opacity as f32)
-            } else {
-                current.active_pane_modifiers = Some(ActivePanelModifiers {
-                    inactive_opacity: Some(opacity as f32),
-                    ..Default::default()
-                })
-            }
+            current
+                .workspace
+                .active_pane_modifiers
+                .get_or_insert_default()
+                .inactive_opacity = Some(opacity as f32);
         }
 
         vscode.enum_setting(
             "window.confirmBeforeClose",
-            &mut current.confirm_quit,
+            &mut current.workspace.confirm_quit,
             |s| match s {
                 "always" | "keyboardOnly" => Some(true),
                 "never" => Some(false),
@@ -331,22 +200,22 @@ impl Settings for WorkspaceSettings {
 
         vscode.bool_setting(
             "workbench.editor.restoreViewState",
-            &mut current.restore_on_file_reopen,
+            &mut current.workspace.restore_on_file_reopen,
         );
 
         if let Some(b) = vscode.read_bool("window.closeWhenEmpty") {
-            current.when_closing_with_no_tabs = Some(if b {
-                CloseWindowWhenNoItems::CloseWindow
+            current.workspace.when_closing_with_no_tabs = Some(if b {
+                settings::CloseWindowWhenNoItems::CloseWindow
             } else {
-                CloseWindowWhenNoItems::KeepWindowOpen
-            })
+                settings::CloseWindowWhenNoItems::KeepWindowOpen
+            });
         }
 
         if let Some(b) = vscode.read_bool("files.simpleDialog.enable") {
-            current.use_system_path_prompts = Some(!b);
+            current.workspace.use_system_path_prompts = Some(!b);
         }
 
-        vscode.enum_setting("files.autoSave", &mut current.autosave, |s| match s {
+        if let Some(v) = vscode.read_enum("files.autoSave", |s| match s {
             "off" => Some(AutosaveSetting::Off),
             "afterDelay" => Some(AutosaveSetting::AfterDelay {
                 milliseconds: vscode
@@ -357,7 +226,9 @@ impl Settings for WorkspaceSettings {
             "onFocusChange" => Some(AutosaveSetting::OnFocusChange),
             "onWindowChange" => Some(AutosaveSetting::OnWindowChange),
             _ => None,
-        });
+        }) {
+            current.workspace.autosave = Some(v);
+        }
 
         // workbench.editor.limit contains "enabled", "value", and "perEditorGroup"
         // our semantics match if those are set to true, some N, and true respectively.
@@ -370,10 +241,12 @@ impl Settings for WorkspaceSettings {
                 .read_bool("workbench.editor.limit.enabled")
                 .unwrap_or_default()
         {
-            current.max_tabs = Some(n)
+            current.workspace.max_tabs = Some(n)
         }
 
-        vscode.bool_setting("window.nativeTabs", &mut current.use_system_window_tabs);
+        if let Some(b) = vscode.read_bool("window.nativeTabs") {
+            current.workspace.use_system_window_tabs = Some(b);
+        }
 
         // some combination of "window.restoreWindows" and "workbench.startupEditor" might
         // map to our "restore_on_startup"
@@ -384,24 +257,39 @@ impl Settings for WorkspaceSettings {
 }
 
 impl Settings for TabBarSettings {
-    type FileContent = TabBarSettingsContent;
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let tab_bar = content.tab_bar.clone().unwrap();
+        TabBarSettings {
+            show: tab_bar.show.unwrap(),
+            show_nav_history_buttons: tab_bar.show_nav_history_buttons.unwrap(),
+            show_tab_bar_buttons: tab_bar.show_tab_bar_buttons.unwrap(),
+        }
+    }
 
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _cx: &mut App) {
+        let Some(tab_bar) = &content.tab_bar else {
+            return;
+        };
+        self.show.merge_from(&tab_bar.show);
+        self.show_nav_history_buttons
+            .merge_from(&tab_bar.show_nav_history_buttons);
+        self.show_tab_bar_buttons
+            .merge_from(&tab_bar.show_tab_bar_buttons);
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
-        vscode.enum_setting(
-            "workbench.editor.showTabs",
-            &mut current.show,
-            |s| match s {
-                "multiple" => Some(true),
-                "single" | "none" => Some(false),
-                _ => None,
-            },
-        );
+    fn import_from_vscode(
+        vscode: &settings::VsCodeSettings,
+        current: &mut settings::SettingsContent,
+    ) {
+        if let Some(b) = vscode.read_enum("workbench.editor.showTabs", |s| match s {
+            "multiple" => Some(true),
+            "single" | "none" => Some(false),
+            _ => None,
+        }) {
+            current.tab_bar.get_or_insert_default().show = Some(b);
+        }
         if Some("hidden") == vscode.read_string("workbench.editor.editorActionsLocation") {
-            current.show_tab_bar_buttons = Some(false)
+            current.tab_bar.get_or_insert_default().show_tab_bar_buttons = Some(false)
         }
     }
 }

crates/worktree/Cargo.toml πŸ”—

@@ -38,7 +38,6 @@ parking_lot.workspace = true
 paths.workspace = true
 postage.workspace = true
 rpc = { workspace = true, features = ["gpui"] }
-schemars.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/worktree/src/worktree_settings.rs πŸ”—

@@ -2,10 +2,8 @@ use std::path::Path;
 
 use anyhow::Context as _;
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
-use util::paths::PathMatcher;
+use settings::{Settings, SettingsContent};
+use util::{ResultExt, paths::PathMatcher};
 
 #[derive(Clone, PartialEq, Eq)]
 pub struct WorktreeSettings {
@@ -32,57 +30,13 @@ impl WorktreeSettings {
     }
 }
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
-#[settings_key(None)]
-pub struct WorktreeSettingsContent {
-    /// The displayed name of this project. If not set, the root directory name
-    /// will be displayed.
-    ///
-    /// Default: none
-    #[serde(default)]
-    pub project_name: Option<String>,
-
-    /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides
-    /// `file_scan_inclusions`.
-    ///
-    /// Default: [
-    ///   "**/.git",
-    ///   "**/.svn",
-    ///   "**/.hg",
-    ///   "**/.jj",
-    ///   "**/CVS",
-    ///   "**/.DS_Store",
-    ///   "**/Thumbs.db",
-    ///   "**/.classpath",
-    ///   "**/.settings"
-    /// ]
-    #[serde(default)]
-    pub file_scan_exclusions: Option<Vec<String>>,
-
-    /// Always include files that match these globs when scanning for files, even if they're
-    /// ignored by git. This setting is overridden by `file_scan_exclusions`.
-    /// Default: [
-    ///  ".env*",
-    ///  "docker-compose.*.yml",
-    /// ]
-    #[serde(default)]
-    pub file_scan_inclusions: Option<Vec<String>>,
-
-    /// Treat the files matching these globs as `.env` files.
-    /// Default: [ "**/.env*" ]
-    pub private_files: Option<Vec<String>>,
-}
-
 impl Settings for WorktreeSettings {
-    type FileContent = WorktreeSettingsContent;
-
-    fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
-        let result: WorktreeSettingsContent = sources.json_merge()?;
-        let mut file_scan_exclusions = result.file_scan_exclusions.unwrap_or_default();
-        let mut private_files = result.private_files.unwrap_or_default();
-        let mut parsed_file_scan_inclusions: Vec<String> = result
-            .file_scan_inclusions
-            .unwrap_or_default()
+    fn from_defaults(content: &settings::SettingsContent, _cx: &mut App) -> Self {
+        let worktree = content.project.worktree.clone();
+        let file_scan_exclusions = worktree.file_scan_exclusions.unwrap();
+        let file_scan_inclusions = worktree.file_scan_inclusions.unwrap();
+        let private_files = worktree.private_files.unwrap();
+        let parsed_file_scan_inclusions: Vec<String> = file_scan_inclusions
             .iter()
             .flat_map(|glob| {
                 Path::new(glob)
@@ -91,30 +45,71 @@ impl Settings for WorktreeSettings {
             })
             .filter(|p: &String| !p.is_empty())
             .collect();
-        file_scan_exclusions.sort();
-        private_files.sort();
-        parsed_file_scan_inclusions.sort();
-        Ok(Self {
-            file_scan_exclusions: path_matchers(&file_scan_exclusions, "file_scan_exclusions")?,
-            private_files: path_matchers(&private_files, "private_files")?,
+
+        Self {
+            project_name: None,
+            file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions")
+                .unwrap(),
             file_scan_inclusions: path_matchers(
-                &parsed_file_scan_inclusions,
+                parsed_file_scan_inclusions,
                 "file_scan_inclusions",
-            )?,
-            project_name: result.project_name,
-        })
+            )
+            .unwrap(),
+            private_files: path_matchers(private_files, "private_files").unwrap(),
+        }
+    }
+
+    fn refine(&mut self, content: &SettingsContent, _cx: &mut App) {
+        let worktree = &content.project.worktree;
+
+        if let Some(project_name) = worktree.project_name.clone() {
+            self.project_name = Some(project_name);
+        }
+
+        if let Some(mut private_files) = worktree.private_files.clone() {
+            let sources = self.private_files.sources();
+            private_files.extend_from_slice(sources);
+            if let Some(matchers) = path_matchers(private_files, "private_files").log_err() {
+                self.private_files = matchers;
+            }
+        }
+
+        if let Some(file_scan_exclusions) = worktree.file_scan_exclusions.clone() {
+            if let Some(matchers) =
+                path_matchers(file_scan_exclusions, "file_scan_exclusions").log_err()
+            {
+                self.file_scan_exclusions = matchers
+            }
+        }
+
+        if let Some(file_scan_inclusions) = worktree.file_scan_inclusions.clone() {
+            let parsed_file_scan_inclusions: Vec<String> = file_scan_inclusions
+                .iter()
+                .flat_map(|glob| {
+                    Path::new(glob)
+                        .ancestors()
+                        .map(|a| a.to_string_lossy().into())
+                })
+                .filter(|p: &String| !p.is_empty())
+                .collect();
+            if let Some(matchers) =
+                path_matchers(parsed_file_scan_inclusions, "file_scan_inclusions").log_err()
+            {
+                self.file_scan_inclusions = matchers
+            }
+        }
     }
 
-    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
+    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) {
         if let Some(inclusions) = vscode
             .read_value("files.watcherInclude")
             .and_then(|v| v.as_array())
             .and_then(|v| v.iter().map(|n| n.as_str().map(str::to_owned)).collect())
         {
-            if let Some(old) = current.file_scan_inclusions.as_mut() {
+            if let Some(old) = current.project.worktree.file_scan_inclusions.as_mut() {
                 old.extend(inclusions)
             } else {
-                current.file_scan_inclusions = Some(inclusions)
+                current.project.worktree.file_scan_inclusions = Some(inclusions)
             }
         }
         if let Some(exclusions) = vscode
@@ -122,15 +117,16 @@ impl Settings for WorktreeSettings {
             .and_then(|v| v.as_array())
             .and_then(|v| v.iter().map(|n| n.as_str().map(str::to_owned)).collect())
         {
-            if let Some(old) = current.file_scan_exclusions.as_mut() {
+            if let Some(old) = current.project.worktree.file_scan_exclusions.as_mut() {
                 old.extend(exclusions)
             } else {
-                current.file_scan_exclusions = Some(exclusions)
+                current.project.worktree.file_scan_exclusions = Some(exclusions)
             }
         }
     }
 }
 
-fn path_matchers(values: &[String], context: &'static str) -> anyhow::Result<PathMatcher> {
+fn path_matchers(mut values: Vec<String>, context: &'static str) -> anyhow::Result<PathMatcher> {
+    values.sort();
     PathMatcher::new(values).with_context(|| format!("Failed to parse globs from {}", context))
 }

crates/worktree/src/worktree_tests.rs πŸ”—

@@ -770,9 +770,9 @@ async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
     }));
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions = Some(vec![]);
-                project_settings.file_scan_inclusions = Some(vec![
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions = Some(vec![]);
+                settings.project.worktree.file_scan_inclusions = Some(vec![
                     "node_modules/**/package.json".to_string(),
                     "**/.DS_Store".to_string(),
                 ]);
@@ -836,9 +836,11 @@ async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext)
 
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions = Some(vec!["**/.DS_Store".to_string()]);
-                project_settings.file_scan_inclusions = Some(vec!["**/.DS_Store".to_string()]);
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions =
+                    Some(vec!["**/.DS_Store".to_string()]);
+                settings.project.worktree.file_scan_inclusions =
+                    Some(vec!["**/.DS_Store".to_string()]);
             });
         });
     });
@@ -894,9 +896,10 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
 
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions = Some(vec![]);
-                project_settings.file_scan_inclusions = Some(vec!["node_modules/**".to_string()]);
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions = Some(vec![]);
+                settings.project.worktree.file_scan_inclusions =
+                    Some(vec!["node_modules/**".to_string()]);
             });
         });
     });
@@ -926,9 +929,9 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
 
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions = Some(vec![]);
-                project_settings.file_scan_inclusions = Some(vec![]);
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions = Some(vec![]);
+                settings.project.worktree.file_scan_inclusions = Some(vec![]);
             });
         });
     });
@@ -978,8 +981,8 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
     }));
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions =
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions =
                     Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
             });
         });
@@ -1015,8 +1018,8 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
 
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions =
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions =
                     Some(vec!["**/node_modules/**".to_string()]);
             });
         });
@@ -1080,8 +1083,8 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
     }));
     cx.update(|cx| {
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                project_settings.file_scan_exclusions = Some(vec![
+            store.update_user_settings(cx, |settings| {
+                settings.project.worktree.file_scan_exclusions = Some(vec![
                     "**/.git".to_string(),
                     "node_modules/".to_string(),
                     "build_output".to_string(),

crates/zed/Cargo.toml πŸ”—

@@ -126,7 +126,6 @@ serde.workspace = true
 serde_json.workspace = true
 session.workspace = true
 settings.workspace = true
-settings_ui.workspace = true
 keymap_editor.workspace = true
 shellexpand.workspace = true
 smol.workspace = true
@@ -185,7 +184,6 @@ itertools.workspace = true
 language = { workspace = true, features = ["test-support"] }
 pretty_assertions.workspace = true
 project = { workspace = true, features = ["test-support"] }
-settings_ui = { workspace = true, features = ["test-support"] }
 terminal_view = { workspace = true, features = ["test-support"] }
 tree-sitter-md.workspace = true
 tree-sitter-rust.workspace = true

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

@@ -614,7 +614,6 @@ pub fn main() {
         markdown_preview::init(cx);
         svg_preview::init(cx);
         onboarding::init(cx);
-        settings_ui::init(cx);
         keymap_editor::init(cx);
         extensions_ui::init(cx);
         zeta::init(cx);

crates/zed/src/zed.rs πŸ”—

@@ -727,9 +727,10 @@ fn register_actions(
             let fs = app_state.fs.clone();
             move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
                 if action.persist {
-                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
+                    update_settings_file(fs.clone(), cx, move |settings, cx| {
                         let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
                         let _ = settings
+                            .theme
                             .ui_font_size
                             .insert(theme::clamp_font_size(ui_font_size).0);
                     });
@@ -742,9 +743,10 @@ fn register_actions(
             let fs = app_state.fs.clone();
             move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
                 if action.persist {
-                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
+                    update_settings_file(fs.clone(), cx, move |settings, cx| {
                         let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
                         let _ = settings
+                            .theme
                             .ui_font_size
                             .insert(theme::clamp_font_size(ui_font_size).0);
                     });
@@ -757,8 +759,8 @@ fn register_actions(
             let fs = app_state.fs.clone();
             move |_, action: &zed_actions::ResetUiFontSize, _window, cx| {
                 if action.persist {
-                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, _| {
-                        settings.ui_font_size = None;
+                    update_settings_file(fs.clone(), cx, move |settings, _| {
+                        settings.theme.ui_font_size = None;
                     });
                 } else {
                     theme::reset_ui_font_size(cx);
@@ -769,10 +771,11 @@ fn register_actions(
             let fs = app_state.fs.clone();
             move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
                 if action.persist {
-                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
+                    update_settings_file(fs.clone(), cx, move |settings, cx| {
                         let buffer_font_size =
                             ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
                         let _ = settings
+                            .theme
                             .buffer_font_size
                             .insert(theme::clamp_font_size(buffer_font_size).0);
                     });
@@ -785,10 +788,11 @@ fn register_actions(
             let fs = app_state.fs.clone();
             move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
                 if action.persist {
-                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, cx| {
+                    update_settings_file(fs.clone(), cx, move |settings, cx| {
                         let buffer_font_size =
                             ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
                         let _ = settings
+                            .theme
                             .buffer_font_size
                             .insert(theme::clamp_font_size(buffer_font_size).0);
                     });
@@ -801,8 +805,8 @@ fn register_actions(
             let fs = app_state.fs.clone();
             move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| {
                 if action.persist {
-                    update_settings_file::<ThemeSettings>(fs.clone(), cx, move |settings, _| {
-                        settings.buffer_font_size = None;
+                    update_settings_file(fs.clone(), cx, move |settings, _| {
+                        settings.theme.buffer_font_size = None;
                     });
                 } else {
                     theme::reset_buffer_font_size(cx);
@@ -1944,7 +1948,7 @@ mod tests {
     };
     use language::{LanguageMatcher, LanguageRegistry};
     use pretty_assertions::{assert_eq, assert_ne};
-    use project::{Project, ProjectPath, WorktreeSettings, project_settings::ProjectSettings};
+    use project::{Project, ProjectPath};
     use serde_json::json;
     use settings::{SettingsStore, watch_config_file};
     use std::{
@@ -2249,8 +2253,11 @@ mod tests {
 
         cx.update(|cx| {
             SettingsStore::update_global(cx, |store, cx| {
-                store.update_user_settings::<ProjectSettings>(cx, |settings| {
-                    settings.session.restore_unsaved_buffers = false
+                store.update_user_settings(cx, |settings| {
+                    settings
+                        .session
+                        .get_or_insert_default()
+                        .restore_unsaved_buffers = Some(false)
                 });
             });
         });
@@ -2965,8 +2972,8 @@ mod tests {
         let app_state = init_test(cx);
         cx.update(|cx| {
             cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
-                    project_settings.file_scan_exclusions =
+                store.update_user_settings(cx, |project_settings| {
+                    project_settings.project.worktree.file_scan_exclusions =
                         Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
                 });
             });
@@ -4795,8 +4802,9 @@ mod tests {
 
         // 3. Add .zed to file scan exclusions in user settings
         cx.update_global::<SettingsStore, _>(|store, cx| {
-            store.update_user_settings::<WorktreeSettings>(cx, |worktree_settings| {
-                worktree_settings.file_scan_exclusions = Some(vec![".zed".to_string()]);
+            store.update_user_settings(cx, |worktree_settings| {
+                worktree_settings.project.worktree.file_scan_exclusions =
+                    Some(vec![".zed".to_string()]);
             });
         });
 
@@ -4856,34 +4864,4 @@ mod tests {
             "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
         );
     }
-
-    #[gpui::test]
-    fn test_settings_defaults(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            settings::init(cx);
-            workspace::init_settings(cx);
-            title_bar::init(cx);
-            editor::init_settings(cx);
-            debugger_ui::init(cx);
-        });
-        let default_json =
-            cx.read(|cx| cx.global::<SettingsStore>().raw_default_settings().clone());
-
-        let all_paths = cx.read(|cx| settings_ui::SettingsUiTree::new(cx).all_paths(cx));
-        let mut failures = Vec::new();
-        for path in all_paths {
-            if settings_ui::read_settings_value_from_path(&default_json, &path).is_none() {
-                failures.push(path);
-            }
-        }
-        if !failures.is_empty() {
-            panic!(
-                "No default value found for paths: {:#?}",
-                failures
-                    .into_iter()
-                    .map(|path| path.join("."))
-                    .collect::<Vec<_>>()
-            );
-        }
-    }
 }

crates/zeta/src/init.rs πŸ”—

@@ -3,7 +3,7 @@ use std::any::{Any, TypeId};
 use command_palette_hooks::CommandPaletteFilter;
 use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag};
 use gpui::actions;
-use language::language_settings::{AllLanguageSettings, EditPredictionProvider};
+use language::language_settings::EditPredictionProvider;
 use project::DisableAiSettings;
 use settings::{Settings, SettingsStore, update_settings_file};
 use ui::App;
@@ -44,15 +44,14 @@ pub fn init(cx: &mut App) {
         );
 
         workspace.register_action(|workspace, _: &ResetOnboarding, _window, cx| {
-            update_settings_file::<AllLanguageSettings>(
-                workspace.app_state().fs.clone(),
-                cx,
-                move |file, _| {
-                    file.features
-                        .get_or_insert(Default::default())
-                        .edit_prediction_provider = Some(EditPredictionProvider::None)
-                },
-            );
+            update_settings_file(workspace.app_state().fs.clone(), cx, move |settings, _| {
+                settings
+                    .project
+                    .all_languages
+                    .features
+                    .get_or_insert_default()
+                    .edit_prediction_provider = Some(EditPredictionProvider::None)
+            });
         });
     })
     .detach();

crates/zeta/src/onboarding_modal.rs πŸ”—

@@ -9,7 +9,7 @@ use gpui::{
     ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
     linear_color_stop, linear_gradient,
 };
-use language::language_settings::{AllLanguageSettings, EditPredictionProvider};
+use language::language_settings::EditPredictionProvider;
 use settings::update_settings_file;
 use ui::{Vector, VectorName, prelude::*};
 use workspace::{ModalView, Workspace};
@@ -22,8 +22,10 @@ pub struct ZedPredictModal {
 
 pub(crate) fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) {
     let fs = <dyn Fs>::global(cx);
-    update_settings_file::<AllLanguageSettings>(fs, cx, move |settings, _| {
+    update_settings_file(fs, cx, move |settings, _| {
         settings
+            .project
+            .all_languages
             .features
             .get_or_insert(Default::default())
             .edit_prediction_provider = Some(provider);

crates/zeta_cli/src/headless.rs πŸ”—

@@ -31,10 +31,7 @@ pub fn init(cx: &mut App) -> ZetaCliAppState {
     release_channel::init(app_version, cx);
     gpui_tokio::init(cx);
 
-    let mut settings_store = SettingsStore::new(cx);
-    settings_store
-        .set_default_settings(settings::default_settings().as_ref(), cx)
-        .unwrap();
+    let settings_store = SettingsStore::new(cx, &settings::default_settings());
     cx.set_global(settings_store);
     client::init_settings(cx);
 

crates/zlog/Cargo.toml πŸ”—

@@ -15,6 +15,7 @@ path = "src/zlog.rs"
 default = []
 
 [dependencies]
+collections.workspace = true
 chrono.workspace = true
 log.workspace = true
 workspace-hack.workspace = true

crates/zlog/src/filter.rs πŸ”—

@@ -1,9 +1,8 @@
-use std::{
-    collections::{HashMap, VecDeque},
-    sync::{
-        OnceLock, RwLock,
-        atomic::{AtomicU8, Ordering},
-    },
+use collections::HashMap;
+use std::collections::VecDeque;
+use std::sync::{
+    OnceLock, RwLock,
+    atomic::{AtomicU8, Ordering},
 };
 
 use crate::{SCOPE_DEPTH_MAX, SCOPE_STRING_SEP_STR, Scope, ScopeAlloc, env_config, private};

crates/zlog_settings/Cargo.toml πŸ”—

@@ -15,10 +15,8 @@ path = "src/zlog_settings.rs"
 default = []
 
 [dependencies]
-anyhow.workspace = true
 gpui.workspace = true
-schemars.workspace = true
-serde.workspace = true
+collections.workspace = true
 settings.workspace = true
 zlog.workspace = true
 workspace-hack.workspace = true

crates/zlog_settings/src/zlog_settings.rs πŸ”—

@@ -1,9 +1,8 @@
 //! # zlog_settings
-use anyhow::Result;
+use collections::HashMap;
+
 use gpui::App;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsStore, SettingsUi};
+use settings::{Settings, SettingsStore};
 
 pub fn init(cx: &mut App) {
     ZlogSettings::register(cx);
@@ -15,33 +14,27 @@ pub fn init(cx: &mut App) {
     .detach();
 }
 
-#[derive(
-    Clone,
-    Debug,
-    Default,
-    Serialize,
-    Deserialize,
-    PartialEq,
-    Eq,
-    JsonSchema,
-    SettingsUi,
-    SettingsKey,
-)]
-#[settings_key(key = "log")]
+#[derive(Clone, Debug)]
 pub struct ZlogSettings {
-    #[serde(default, flatten)]
-    pub scopes: std::collections::HashMap<String, String>,
+    /// A map of log scopes to the desired log level.
+    /// Useful for filtering out noisy logs or enabling more verbose logging.
+    ///
+    /// Example: {"log": {"client": "warn"}}
+    pub scopes: HashMap<String, String>,
 }
 
 impl Settings for ZlogSettings {
-    type FileContent = Self;
+    fn from_defaults(content: &settings::SettingsContent, _: &mut App) -> Self {
+        ZlogSettings {
+            scopes: content.log.clone().unwrap(),
+        }
+    }
 
-    fn load(sources: settings::SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self>
-    where
-        Self: Sized,
-    {
-        sources.json_merge()
+    fn refine(&mut self, content: &settings::SettingsContent, _: &mut App) {
+        if let Some(log) = &content.log {
+            self.scopes.extend(log.clone());
+        }
     }
 
-    fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+    fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut settings::SettingsContent) {}
 }