Merge pull request #2448 from zed-industries/setting-store

Max Brunsfeld created

Separate the settings struct into a set of dynamically-registered setting types

Change summary

Cargo.lock                                              |  351 +-
Cargo.toml                                              |    2 
assets/settings/default.json                            |    9 
crates/activity_indicator/Cargo.toml                    |    2 
crates/activity_indicator/src/activity_indicator.rs     |    8 
crates/auto_update/src/auto_update.rs                   |   35 
crates/auto_update/src/update_notification.rs           |    3 
crates/breadcrumbs/src/breadcrumbs.rs                   |    3 
crates/client/Cargo.toml                                |    1 
crates/client/src/client.rs                             |   90 
crates/client/src/telemetry.rs                          |   11 
crates/client/src/user.rs                               |   13 
crates/collab/Cargo.toml                                |    2 
crates/collab/src/tests.rs                              |   12 
crates/collab/src/tests/integration_tests.rs            |   56 
crates/collab/src/tests/randomized_integration_tests.rs |    5 
crates/collab_ui/src/collab_titlebar_item.rs            |    7 
crates/collab_ui/src/contact_finder.rs                  |    3 
crates/collab_ui/src/contact_list.rs                    |    5 
crates/collab_ui/src/contacts_popover.rs                |    3 
crates/collab_ui/src/incoming_call_notification.rs      |   28 
crates/collab_ui/src/notifications.rs                   |    3 
crates/collab_ui/src/project_shared_notification.rs     |   27 
crates/collab_ui/src/sharing_status_indicator.rs        |    6 
crates/command_palette/Cargo.toml                       |    1 
crates/command_palette/src/command_palette.rs           |   25 
crates/context_menu/src/context_menu.rs                 |    5 
crates/copilot/src/copilot.rs                           |   87 
crates/copilot/src/sign_in.rs                           |    5 
crates/copilot_button/Cargo.toml                        |    2 
crates/copilot_button/src/copilot_button.rs             |   99 
crates/diagnostics/Cargo.toml                           |    1 
crates/diagnostics/src/diagnostics.rs                   |   55 
crates/diagnostics/src/items.rs                         |   10 
crates/editor/Cargo.toml                                |    2 
crates/editor/src/blink_manager.rs                      |   12 
crates/editor/src/display_map.rs                        |   65 
crates/editor/src/display_map/block_map.rs              |   13 
crates/editor/src/display_map/fold_map.rs               |   14 
crates/editor/src/display_map/suggestion_map.rs         |   10 
crates/editor/src/display_map/wrap_map.rs               |   14 
crates/editor/src/editor.rs                             |  156 
crates/editor/src/editor_settings.rs                    |   43 
crates/editor/src/editor_tests.rs                       |  365 +
crates/editor/src/element.rs                            |  120 
crates/editor/src/highlight_matching_bracket.rs         |    4 
crates/editor/src/hover_popover.rs                      |   20 
crates/editor/src/items.rs                              |    3 
crates/editor/src/link_go_to_definition.rs              |   16 
crates/editor/src/mouse_context_menu.rs                 |    5 
crates/editor/src/movement.rs                           |   33 
crates/editor/src/multi_buffer.rs                       |   29 
crates/editor/src/test/editor_lsp_test_context.rs       |    8 
crates/editor/src/test/editor_test_context.rs           |   24 
crates/feedback/src/deploy_feedback_button.rs           |    3 
crates/feedback/src/feedback_info_text.rs               |    3 
crates/feedback/src/submit_feedback_button.rs           |    3 
crates/file_finder/Cargo.toml                           |    4 
crates/file_finder/src/file_finder.rs                   |   93 
crates/go_to_line/Cargo.toml                            |    1 
crates/go_to_line/src/go_to_line.rs                     |    3 
crates/gpui/src/app.rs                                  |   11 
crates/gpui/src/executor.rs                             |   16 
crates/journal/Cargo.toml                               |    5 
crates/journal/src/journal.rs                           |   56 
crates/language/Cargo.toml                              |    3 
crates/language/src/buffer.rs                           |   17 
crates/language/src/buffer_tests.rs                     |   73 
crates/language/src/language.rs                         |    5 
crates/language/src/language_settings.rs                |  337 ++
crates/language_selector/src/active_buffer_language.rs  |    3 
crates/language_selector/src/language_selector.rs       |    4 
crates/lsp_log/src/lsp_log.rs                           |    3 
crates/outline/Cargo.toml                               |    2 
crates/outline/src/outline.rs                           |    7 
crates/picker/src/picker.rs                             |    2 
crates/project/Cargo.toml                               |    3 
crates/project/src/project.rs                           |   71 
crates/project/src/project_settings.rs                  |   31 
crates/project/src/project_tests.rs                     |  144 
crates/project/src/terminals.rs                         |   22 
crates/project_panel/Cargo.toml                         |    2 
crates/project_panel/src/project_panel.rs               |   42 
crates/project_symbols/Cargo.toml                       |    4 
crates/project_symbols/src/project_symbols.rs           |   25 
crates/recent_projects/Cargo.toml                       |    1 
crates/recent_projects/src/recent_projects.rs           |    6 
crates/search/Cargo.toml                                |    1 
crates/search/src/buffer_search.rs                      |   35 
crates/search/src/project_search.rs                     |   62 
crates/settings/Cargo.toml                              |   12 
crates/settings/src/keymap_file.rs                      |   20 
crates/settings/src/settings.rs                         | 1615 ----------
crates/settings/src/settings_file.rs                    |  457 --
crates/settings/src/settings_store.rs                   | 1246 ++++++++
crates/settings/src/watched_json.rs                     |  126 
crates/terminal/Cargo.toml                              |    2 
crates/terminal/src/terminal.rs                         |  126 
crates/terminal_view/src/terminal_button.rs             |    3 
crates/terminal_view/src/terminal_element.rs            |   74 
crates/terminal_view/src/terminal_view.rs               |   92 
crates/theme/Cargo.toml                                 |   18 
crates/theme/src/theme.rs                               |   27 
crates/theme/src/theme_registry.rs                      |   17 
crates/theme/src/theme_settings.rs                      |  184 +
crates/theme_selector/Cargo.toml                        |    1 
crates/theme_selector/src/theme_selector.rs             |   54 
crates/theme_testbench/src/theme_testbench.rs           |    9 
crates/util/src/util.rs                                 |   21 
crates/vim/Cargo.toml                                   |    1 
crates/vim/src/test/vim_test_context.rs                 |   19 
crates/vim/src/vim.rs                                   |   32 
crates/welcome/Cargo.toml                               |    9 
crates/welcome/src/base_keymap_picker.rs                |   26 
crates/welcome/src/base_keymap_setting.rs               |   65 
crates/welcome/src/welcome.rs                           |   56 
crates/workspace/Cargo.toml                             |    1 
crates/workspace/src/dock.rs                            |   20 
crates/workspace/src/dock/toggle_dock_button.rs         |    3 
crates/workspace/src/item.rs                            |   17 
crates/workspace/src/notifications.rs                   |    9 
crates/workspace/src/pane.rs                            |   51 
crates/workspace/src/pane/dragged_item_receiver.rs      |   12 
crates/workspace/src/pane_group.rs                      |    5 
crates/workspace/src/persistence.rs                     |    9 
crates/workspace/src/persistence/model.rs               |   10 
crates/workspace/src/shared_screen.rs                   |    3 
crates/workspace/src/sidebar.rs                         |    5 
crates/workspace/src/status_bar.rs                      |    3 
crates/workspace/src/toolbar.rs                         |    5 
crates/workspace/src/workspace.rs                       |  110 
crates/workspace/src/workspace_settings.rs              |  103 
crates/zed/Cargo.toml                                   |    2 
crates/zed/src/languages.rs                             |    8 
crates/zed/src/languages/c.rs                           |   15 
crates/zed/src/languages/json.rs                        |   33 
crates/zed/src/languages/python.rs                      |   15 
crates/zed/src/languages/rust.rs                        |   15 
crates/zed/src/languages/yaml.rs                        |    8 
crates/zed/src/main.rs                                  |  116 
crates/zed/src/zed.rs                                   |  307 +
141 files changed, 4,518 insertions(+), 3,713 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -14,12 +14,13 @@ version = "0.1.0"
 dependencies = [
  "auto_update",
  "editor",
- "futures 0.3.25",
+ "futures 0.3.28",
  "gpui",
  "language",
  "project",
  "settings",
  "smallvec",
+ "theme",
  "util",
  "workspace",
 ]
@@ -30,7 +31,16 @@ version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
 dependencies = [
- "gimli",
+ "gimli 0.26.2",
+]
+
+[[package]]
+name = "addr2line"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
+dependencies = [
+ "gimli 0.27.2",
 ]
 
 [[package]]
@@ -51,7 +61,18 @@ version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
 dependencies = [
- "getrandom 0.2.8",
+ "getrandom 0.2.9",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
+dependencies = [
+ "cfg-if 1.0.0",
  "once_cell",
  "version_check",
 ]
@@ -65,6 +86,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "aho-corasick"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "alacritty_config"
 version = "0.1.1-dev"
@@ -82,7 +112,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=a51dbe25d67e84d6ed
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -92,7 +122,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=a51dbe25d67e84d6ed
 dependencies = [
  "alacritty_config",
  "alacritty_config_derive",
- "base64",
+ "base64 0.13.1",
  "bitflags",
  "dirs 4.0.0",
  "libc",
@@ -145,15 +175,15 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.66"
+version = "1.0.71"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 
 [[package]]
 name = "arrayref"
-version = "0.3.6"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
 
 [[package]]
 name = "arrayvec"
@@ -232,9 +262,9 @@ dependencies = [
 
 [[package]]
 name = "async-executor"
-version = "1.5.0"
+version = "1.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b"
+checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb"
 dependencies = [
  "async-lock",
  "async-task",
@@ -273,32 +303,31 @@ dependencies = [
 
 [[package]]
 name = "async-io"
-version = "1.12.0"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
 dependencies = [
  "async-lock",
  "autocfg 1.1.0",
+ "cfg-if 1.0.0",
  "concurrent-queue",
  "futures-lite",
- "libc",
  "log",
  "parking",
  "polling",
+ "rustix 0.37.19",
  "slab",
  "socket2",
  "waker-fn",
- "windows-sys 0.42.0",
 ]
 
 [[package]]
 name = "async-lock"
-version = "2.6.0"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685"
+checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
 dependencies = [
  "event-listener",
- "futures-lite",
 ]
 
 [[package]]
@@ -318,15 +347,15 @@ name = "async-pipe"
 version = "0.1.3"
 source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553"
 dependencies = [
- "futures 0.3.25",
+ "futures 0.3.28",
  "log",
 ]
 
 [[package]]
 name = "async-process"
-version = "1.6.0"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4"
+checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9"
 dependencies = [
  "async-io",
  "async-lock",
@@ -335,9 +364,9 @@ dependencies = [
  "cfg-if 1.0.0",
  "event-listener",
  "futures-lite",
- "libc",
+ "rustix 0.37.19",
  "signal-hook",
- "windows-sys 0.42.0",
+ "windows-sys 0.48.0",
 ]
 
 [[package]]
@@ -348,18 +377,18 @@ checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
 name = "async-recursion"
-version = "1.0.0"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea"
+checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.15",
 ]
 
 [[package]]
@@ -372,7 +401,7 @@ dependencies = [
  "async-global-executor",
  "async-io",
  "async-lock",
- "crossbeam-utils 0.8.14",
+ "crossbeam-utils 0.8.15",
  "futures-channel",
  "futures-core",
  "futures-io",
@@ -390,23 +419,24 @@ dependencies = [
 
 [[package]]
 name = "async-stream"
-version = "0.3.3"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
 dependencies = [
  "async-stream-impl",
  "futures-core",
+ "pin-project-lite 0.2.9",
 ]
 
 [[package]]
 name = "async-stream-impl"
-version = "0.3.3"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.15",
 ]
 
 [[package]]
@@ -419,7 +449,7 @@ dependencies = [
  "filetime",
  "libc",
  "pin-project",
- "redox_syscall",
+ "redox_syscall 0.2.16",
  "xattr",
 ]
 
@@ -443,13 +473,13 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.59"
+version = "0.1.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364"
+checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.15",
 ]
 
 [[package]]
@@ -486,9 +516,9 @@ dependencies = [
 
 [[package]]
 name = "atomic-waker"
-version = "1.0.0"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"
+checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3"
 
 [[package]]
 name = "atty"
@@ -548,9 +578,9 @@ checksum = "acee9fd5073ab6b045a275b3e709c163dd36c90685219cb21804a147b58dba43"
 dependencies = [
  "async-trait",
  "axum-core",
- "base64",
+ "base64 0.13.1",
  "bitflags",
- "bytes 1.3.0",
+ "bytes 1.4.0",
  "futures-util",
  "headers",
  "http",
@@ -582,7 +612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "37e5939e02c56fecd5c017c37df4238c0a839fa76b7f97acdd7efb804fd181cc"
 dependencies = [
  "async-trait",
- "bytes 1.3.0",
+ "bytes 1.4.0",
  "futures-util",
  "http",
  "http-body",
@@ -598,7 +628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb"
 dependencies = [
  "axum",
- "bytes 1.3.0",
+ "bytes 1.4.0",
  "futures-util",
  "http",
  "mime",
@@ -614,16 +644,16 @@ dependencies = [
 
 [[package]]
 name = "backtrace"
-version = "0.3.66"
+version = "0.3.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
+checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
 dependencies = [
- "addr2line",
+ "addr2line 0.19.0",
  "cc",
  "cfg-if 1.0.0",
  "libc",
- "miniz_oxide 0.5.4",
- "object 0.29.0",
+ "miniz_oxide 0.6.2",
+ "object 0.30.3",
  "rustc-demangle",
 ]
 
@@ -637,7 +667,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -646,11 +676,17 @@ version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
 
+[[package]]
+name = "base64"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
+
 [[package]]
 name = "base64ct"
-version = "1.5.3"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
 [[package]]
 name = "bincode"
@@ -671,7 +707,7 @@ dependencies = [
  "cexpr",
  "clang-sys",
  "clap 2.34.0",
- "env_logger",
+ "env_logger 0.9.3",
  "lazy_static",
  "lazycell",
  "log",
@@ -707,18 +743,18 @@ dependencies = [
 
 [[package]]
 name = "block-buffer"
-version = "0.10.3"
+version = "0.10.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
 dependencies = [
  "generic-array",
 ]
 
 [[package]]
 name = "blocking"
-version = "1.3.0"
+version = "1.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8"
+checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65"
 dependencies = [
  "async-channel",
  "async-lock",
@@ -726,51 +762,52 @@ dependencies = [
  "atomic-waker",
  "fastrand",
  "futures-lite",
+ "log",
 ]
 
 [[package]]
 name = "borsh"
-version = "0.9.3"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa"
+checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b"
 dependencies = [
  "borsh-derive",
- "hashbrown 0.11.2",
+ "hashbrown 0.13.2",
 ]
 
 [[package]]
 name = "borsh-derive"
-version = "0.9.3"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775"
+checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7"
 dependencies = [
  "borsh-derive-internal",
  "borsh-schema-derive-internal",
  "proc-macro-crate",
  "proc-macro2",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
 name = "borsh-derive-internal"
-version = "0.9.3"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065"
+checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
 name = "borsh-schema-derive-internal"
-version = "0.9.3"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0"
+checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -803,45 +840,47 @@ dependencies = [
 
 [[package]]
 name = "bstr"
-version = "0.2.17"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
 dependencies = [
  "memchr",
+ "serde",
 ]
 
 [[package]]
 name = "bumpalo"
-version = "3.11.1"
+version = "3.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b"
 
 [[package]]
 name = "bytecheck"
-version = "0.6.9"
+version = "0.6.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d11cac2c12b5adc6570dad2ee1b87eff4955dac476fe12d81e5fdd352e52406f"
+checksum = "13fe11640a23eb24562225322cd3e452b93a3d4091d62fab69c70542fcd17d1f"
 dependencies = [
  "bytecheck_derive",
  "ptr_meta",
+ "simdutf8",
 ]
 
 [[package]]
 name = "bytecheck_derive"
-version = "0.6.9"
+version = "0.6.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13e576ebe98e605500b3c8041bb888e966653577172df6dd97398714eb30b9bf"
+checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
 name = "bytemuck"
-version = "1.12.3"
+version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f"
+checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
 
 [[package]]
 name = "byteorder"
@@ -861,9 +900,9 @@ dependencies = [
 
 [[package]]
 name = "bytes"
-version = "1.3.0"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
 
 [[package]]
 name = "call"
@@ -874,7 +913,7 @@ dependencies = [
  "client",
  "collections",
  "fs",
- "futures 0.3.25",
+ "futures 0.3.28",
  "gpui",
  "language",
  "live_kit_client",
@@ -894,7 +933,7 @@ checksum = "e54b86398b5852ddd45784b1d9b196b98beb39171821bad4b8b44534a1e87927"
 dependencies = [
  "cap-primitives",
  "cap-std",
- "io-lifetimes",
+ "io-lifetimes 0.5.3",
  "winapi 0.3.9",
 ]
 
@@ -905,13 +944,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fb8fca3e81fae1d91a36e9784ca22a39ef623702b5f7904d89dc31f10184a178"
 dependencies = [
  "ambient-authority",
- "errno",
+ "errno 0.2.8",
  "fs-set-times",
  "io-extras",
- "io-lifetimes",
+ "io-lifetimes 0.5.3",
  "ipnet",
  "maybe-owned",
- "rustix",
+ "rustix 0.33.7",
  "winapi 0.3.9",
  "winapi-util",
  "winx",
@@ -935,9 +974,9 @@ checksum = "2247568946095c7765ad2b441a56caffc08027734c634a6d5edda648f04e32eb"
 dependencies = [
  "cap-primitives",
  "io-extras",
- "io-lifetimes",
+ "io-lifetimes 0.5.3",
  "ipnet",
- "rustix",
+ "rustix 0.33.7",
 ]
 
 [[package]]
@@ -948,7 +987,7 @@ checksum = "c50472b6ebc302af0401fa3fb939694cd8ff00e0d4c9182001e434fc822ab83a"
 dependencies = [
  "cap-primitives",
  "once_cell",
- "rustix",
+ "rustix 0.33.7",
  "winx",
 ]
 
@@ -960,9 +999,9 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
 
 [[package]]
 name = "cc"
-version = "1.0.77"
+version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
 dependencies = [
  "jobserver",
 ]
@@ -990,9 +1029,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chrono"
-version = "0.4.23"
+version = "0.4.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
+checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
 dependencies = [
  "iana-time-zone",
  "js-sys",
@@ -1006,9 +1045,9 @@ dependencies = [
 
 [[package]]
 name = "chunked_transfer"
-version = "1.4.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
+checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a"
 
 [[package]]
 name = "cipher"
@@ -1021,9 +1060,9 @@ dependencies = [
 
 [[package]]
 name = "clang-sys"
-version = "1.4.0"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3"
+checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
 dependencies = [
  "glob",
  "libc",
@@ -1047,9 +1086,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "3.2.23"
+version = "3.2.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
+checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
 dependencies = [
  "atty",
  "bitflags",
@@ -1064,15 +1103,15 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "3.2.18"
+version = "3.2.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
+checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
 dependencies = [
- "heck 0.4.0",
+ "heck 0.4.1",
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1089,7 +1128,7 @@ name = "cli"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "clap 3.2.23",
+ "clap 3.2.25",
  "core-foundation",
  "core-services",
  "dirs 3.0.2",
@@ -1109,7 +1148,7 @@ dependencies = [
  "async-tungstenite",
  "collections",
  "db",
- "futures 0.3.25",
+ "futures 0.3.28",
  "gpui",
  "image",
  "lazy_static",
@@ -1118,6 +1157,7 @@ dependencies = [
  "postage",
  "rand 0.8.5",
  "rpc",
+ "schemars",
  "serde",
  "serde_derive",
  "settings",
@@ -1126,11 +1166,11 @@ dependencies = [
  "sum_tree",
  "tempfile",
  "thiserror",
- "time 0.3.17",
+ "time 0.3.21",
  "tiny_http",
  "url",
  "util",
- "uuid 1.2.2",
+ "uuid 1.3.2",
 ]
 
 [[package]]
@@ -1142,9 +1182,9 @@ dependencies = [
 
 [[package]]
 name = "cmake"
-version = "0.1.49"
+version = "0.1.50"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c"
+checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
 dependencies = [
  "cc",
 ]
@@ -1196,18 +1236,18 @@ dependencies = [
  "async-tungstenite",
  "axum",
  "axum-extra",
- "base64",
+ "base64 0.13.1",
  "call",
- "clap 3.2.23",
+ "clap 3.2.25",
  "client",
  "collections",
  "ctor",
  "dashmap",
  "editor",
- "env_logger",
+ "env_logger 0.9.3",
  "envy",
  "fs",
- "futures 0.3.25",
+ "futures 0.3.28",
  "git",
  "gpui",
  "hyper",
@@ -1237,7 +1277,7 @@ dependencies = [
  "sha-1 0.9.8",
  "sqlx",
  "theme",
- "time 0.3.17",
+ "time 0.3.21",
  "tokio",
  "tokio-tungstenite",
  "toml",
@@ -1264,7 +1304,7 @@ dependencies = [
  "context_menu",
  "editor",
  "feedback",
- "futures 0.3.25",
+ "futures 0.3.28",
  "fuzzy",
  "gpui",
  "log",
@@ -1300,9 +1340,10 @@ dependencies = [
  "collections",
  "ctor",
  "editor",
- "env_logger",
+ "env_logger 0.9.3",
  "fuzzy",
  "gpui",
+ "language",
  "picker",
  "project",
  "serde_json",
@@ -1314,11 +1355,11 @@ dependencies = [
 
 [[package]]
 name = "concurrent-queue"
-version = "2.0.0"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b"
+checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
 dependencies = [
- "crossbeam-utils 0.8.14",
+ "crossbeam-utils 0.8.15",
 ]
 
 [[package]]
@@ -1349,7 +1390,7 @@ dependencies = [
  "collections",
  "context_menu",
  "fs",
- "futures 0.3.25",
+ "futures 0.3.28",
  "gpui",
  "language",
  "log",
@@ -1373,8 +1414,10 @@ dependencies = [
  "context_menu",
  "copilot",
  "editor",
- "futures 0.3.25",
+ "fs",
+ "futures 0.3.28",
  "gpui",
+ "language",
  "settings",
  "smol",
  "theme",
@@ -1452,9 +1495,9 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.5"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
 dependencies = [
  "libc",
 ]
@@ -1479,7 +1522,7 @@ dependencies = [
  "cranelift-codegen-shared",
  "cranelift-entity",
  "cranelift-isle",
- "gimli",
+ "gimli 0.26.2",
  "log",
  "regalloc2",
  "smallvec",
@@ -1557,18 +1600,18 @@ dependencies = [
 
 [[package]]
 name = "crc"
-version = "3.0.0"
+version = "3.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3"
+checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe"
 dependencies = [
  "crc-catalog",
 ]
 
 [[package]]
 name = "crc-catalog"
-version = "2.1.0"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff"
+checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484"
 
 [[package]]
 name = "crc32fast"
@@ -1591,35 +1634,35 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.6"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.14",
+ "crossbeam-utils 0.8.15",
 ]
 
 [[package]]
 name = "crossbeam-deque"
-version = "0.8.2"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-epoch",
- "crossbeam-utils 0.8.14",
+ "crossbeam-utils 0.8.15",
 ]
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.13"
+version = "0.9.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
+checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
 dependencies = [
  "autocfg 1.1.0",
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.14",
- "memoffset 0.7.1",
+ "crossbeam-utils 0.8.15",
+ "memoffset 0.8.0",
  "scopeguard",
 ]
 
@@ -1630,7 +1673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
 dependencies = [
  "cfg-if 1.0.0",
- "crossbeam-utils 0.8.14",
+ "crossbeam-utils 0.8.15",
 ]
 
 [[package]]
@@ -1646,9 +1689,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.14"
+version = "0.8.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
+checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -1680,7 +1723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
 dependencies = [
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1700,9 +1743,9 @@ dependencies = [
 
 [[package]]
 name = "curl-sys"
-version = "0.4.59+curl-7.86.0"
+version = "0.4.61+curl-8.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407"
+checksum = "14d05c10f541ae6f3bc5b3d923c20001f47db7d5f0b2bc6ad16490133842db79"
 dependencies = [
  "cc",
  "libc",
@@ -1716,9 +1759,9 @@ dependencies = [
 
 [[package]]
 name = "cxx"
-version = "1.0.83"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf"
+checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93"
 dependencies = [
  "cc",
  "cxxbridge-flags",
@@ -1728,9 +1771,9 @@ dependencies = [
 
 [[package]]
 name = "cxx-build"
-version = "1.0.83"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39"
+checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b"
 dependencies = [
  "cc",
  "codespan-reporting",
@@ -1738,24 +1781,24 @@ dependencies = [
  "proc-macro2",
  "quote",
  "scratch",
- "syn",
+ "syn 2.0.15",
 ]
 
 [[package]]
 name = "cxxbridge-flags"
-version = "1.0.83"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12"
+checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb"
 
 [[package]]
 name = "cxxbridge-macro"
-version = "1.0.83"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
+checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.15",
 ]
 
 [[package]]

Cargo.toml 🔗

@@ -85,6 +85,7 @@ parking_lot = { version = "0.11.1" }
 postage = { version = "0.5", features = ["futures-traits"] }
 rand = { version = "0.8.5" }
 regex = { version = "1.5" }
+schemars = { version = "0.8" }
 serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
 serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
@@ -93,6 +94,7 @@ smol = { version = "1.2" }
 tempdir = { version = "0.3.7" }
 thiserror = { version = "1.0.29" }
 time = { version = "0.3", features = ["serde", "serde-well-known"] }
+toml = { version = "0.5" }
 unindent = { version = "0.1.7" }
 
 [patch.crates-io]

assets/settings/default.json 🔗

@@ -1,6 +1,15 @@
 {
   // The name of the Zed theme to use for the UI
   "theme": "One Dark",
+  // The name of a base set of key bindings to use.
+  // This setting can take four values, each named after another
+  // text editor:
+  //
+  // 1. "VSCode"
+  // 2. "JetBrains"
+  // 3. "SublimeText"
+  // 4. "Atom"
+  "base_keymap": "VSCode",
   // Features that can be globally enabled or disabled
   "features": {
     // Show Copilot icon in status bar

crates/activity_indicator/Cargo.toml 🔗

@@ -16,6 +16,8 @@ gpui = { path = "../gpui" }
 project = { path = "../project" }
 settings = { path = "../settings" }
 util = { path = "../util" }
+theme = { path = "../theme" }
 workspace = { path = "../workspace" }
+
 futures.workspace = true
 smallvec.workspace = true

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -9,7 +9,6 @@ use gpui::{
 };
 use language::{LanguageRegistry, LanguageServerBinaryStatus};
 use project::{LanguageServerProgress, Project};
-use settings::Settings;
 use smallvec::SmallVec;
 use std::{cmp::Reverse, fmt::Write, sync::Arc};
 use util::ResultExt;
@@ -325,12 +324,7 @@ impl View for ActivityIndicator {
         } = self.content_to_render(cx);
 
         let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
-            let theme = &cx
-                .global::<Settings>()
-                .theme
-                .workspace
-                .status_bar
-                .lsp_status;
+            let theme = &theme::current(cx).workspace.status_bar.lsp_status;
             let style = if state.hovered() && on_click.is_some() {
                 theme.hover.as_ref().unwrap_or(&theme.default)
             } else {

crates/auto_update/src/auto_update.rs 🔗

@@ -1,7 +1,7 @@
 mod update_notification;
 
 use anyhow::{anyhow, Context, Result};
-use client::{Client, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
+use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
     actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
@@ -10,7 +10,7 @@ use gpui::{
 use isahc::AsyncBody;
 use serde::Deserialize;
 use serde_derive::Serialize;
-use settings::Settings;
+use settings::{Setting, SettingsStore};
 use smol::{fs::File, io::AsyncReadExt, process::Command};
 use std::{ffi::OsString, sync::Arc, time::Duration};
 use update_notification::UpdateNotification;
@@ -58,18 +58,37 @@ impl Entity for AutoUpdater {
     type Event = ();
 }
 
+struct AutoUpdateSetting(bool);
+
+impl Setting for AutoUpdateSetting {
+    const KEY: Option<&'static str> = Some("auto_update");
+
+    type FileContent = Option<bool>;
+
+    fn load(
+        default_value: &Option<bool>,
+        user_values: &[&Option<bool>],
+        _: &AppContext,
+    ) -> Result<Self> {
+        Ok(Self(
+            Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
+        ))
+    }
+}
+
 pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
+    settings::register::<AutoUpdateSetting>(cx);
+
     if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
         let auto_updater = cx.add_model(|cx| {
             let updater = AutoUpdater::new(version, http_client, server_url);
 
-            let mut update_subscription = cx
-                .global::<Settings>()
-                .auto_update
+            let mut update_subscription = settings::get::<AutoUpdateSetting>(cx)
+                .0
                 .then(|| updater.start_polling(cx));
 
-            cx.observe_global::<Settings, _>(move |updater, cx| {
-                if cx.global::<Settings>().auto_update {
+            cx.observe_global::<SettingsStore, _>(move |updater, cx| {
+                if settings::get::<AutoUpdateSetting>(cx).0 {
                     if update_subscription.is_none() {
                         update_subscription = Some(updater.start_polling(cx))
                     }
@@ -262,7 +281,7 @@ impl AutoUpdater {
             let release_channel = cx
                 .has_global::<ReleaseChannel>()
                 .then(|| cx.global::<ReleaseChannel>().display_name());
-            let telemetry = cx.global::<Settings>().telemetry().metrics();
+            let telemetry = settings::get::<TelemetrySettings>(cx).metrics;
 
             (installation_id, release_channel, telemetry)
         });

crates/auto_update/src/update_notification.rs 🔗

@@ -5,7 +5,6 @@ use gpui::{
     Element, Entity, View, ViewContext,
 };
 use menu::Cancel;
-use settings::Settings;
 use util::channel::ReleaseChannel;
 use workspace::notifications::Notification;
 
@@ -27,7 +26,7 @@ impl View for UpdateNotification {
     }
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
         let theme = &theme.update_notification;
 
         let app_name = cx.global::<ReleaseChannel>().display_name();

crates/breadcrumbs/src/breadcrumbs.rs 🔗

@@ -4,7 +4,6 @@ use gpui::{
 };
 use itertools::Itertools;
 use search::ProjectSearchView;
-use settings::Settings;
 use workspace::{
     item::{ItemEvent, ItemHandle},
     ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -50,7 +49,7 @@ impl View for Breadcrumbs {
         };
         let not_editor = active_item.downcast::<editor::Editor>().is_none();
 
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
         let style = &theme.workspace.breadcrumbs;
 
         let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {

crates/client/Cargo.toml 🔗

@@ -31,6 +31,7 @@ log.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 rand.workspace = true
+schemars.workspace = true
 smol.workspace = true
 thiserror.workspace = true
 time.workspace = true

crates/client/src/client.rs 🔗

@@ -15,19 +15,17 @@ use futures::{
     TryStreamExt,
 };
 use gpui::{
-    actions,
-    platform::AppVersion,
-    serde_json::{self},
-    AnyModelHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
-    ModelHandle, Task, View, ViewContext, WeakViewHandle,
+    actions, platform::AppVersion, serde_json, AnyModelHandle, AnyWeakModelHandle,
+    AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View, ViewContext,
+    WeakViewHandle,
 };
 use lazy_static::lazy_static;
 use parking_lot::RwLock;
 use postage::watch;
 use rand::prelude::*;
 use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
-use serde::Deserialize;
-use settings::Settings;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
 use std::{
     any::TypeId,
     collections::HashMap,
@@ -72,25 +70,34 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
 
 actions!(client, [SignIn, SignOut]);
 
-pub fn init(client: Arc<Client>, cx: &mut AppContext) {
+pub fn init_settings(cx: &mut AppContext) {
+    settings::register::<TelemetrySettings>(cx);
+}
+
+pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
+    init_settings(cx);
+
+    let client = Arc::downgrade(client);
     cx.add_global_action({
         let client = client.clone();
         move |_: &SignIn, cx| {
-            let client = client.clone();
-            cx.spawn(
-                |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
-            )
-            .detach();
+            if let Some(client) = client.upgrade() {
+                cx.spawn(
+                    |cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
+                )
+                .detach();
+            }
         }
     });
     cx.add_global_action({
         let client = client.clone();
         move |_: &SignOut, cx| {
-            let client = client.clone();
-            cx.spawn(|cx| async move {
-                client.disconnect(&cx);
-            })
-            .detach();
+            if let Some(client) = client.upgrade() {
+                cx.spawn(|cx| async move {
+                    client.disconnect(&cx);
+                })
+                .detach();
+            }
         }
     });
 }
@@ -326,6 +333,42 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
     }
 }
 
+#[derive(Copy, Clone)]
+pub struct TelemetrySettings {
+    pub diagnostics: bool,
+    pub metrics: bool,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct TelemetrySettingsContent {
+    pub diagnostics: Option<bool>,
+    pub metrics: Option<bool>,
+}
+
+impl settings::Setting for TelemetrySettings {
+    const KEY: Option<&'static str> = Some("telemetry");
+
+    type FileContent = TelemetrySettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &AppContext,
+    ) -> Result<Self> {
+        Ok(Self {
+            diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or(
+                default_value
+                    .diagnostics
+                    .ok_or_else(Self::missing_default)?,
+            ),
+            metrics: user_values
+                .first()
+                .and_then(|v| v.metrics)
+                .unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?),
+        })
+    }
+}
+
 impl Client {
     pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
         Arc::new(Self {
@@ -447,9 +490,7 @@ impl Client {
                 }));
             }
             Status::SignedOut | Status::UpgradeRequired => {
-                let telemetry_settings = cx.read(|cx| cx.global::<Settings>().telemetry());
-                self.telemetry
-                    .set_authenticated_user_info(None, false, telemetry_settings);
+                cx.read(|cx| self.telemetry.set_authenticated_user_info(None, false, cx));
                 state._reconnect_task.take();
             }
             _ => {}
@@ -740,7 +781,7 @@ impl Client {
                     self.telemetry().report_mixpanel_event(
                         "read credentials from keychain",
                         Default::default(),
-                        cx.global::<Settings>().telemetry(),
+                        *settings::get::<TelemetrySettings>(cx),
                     );
                 });
             }
@@ -1033,7 +1074,8 @@ impl Client {
         let executor = cx.background();
         let telemetry = self.telemetry.clone();
         let http = self.http.clone();
-        let metrics_enabled = cx.read(|cx| cx.global::<Settings>().telemetry());
+
+        let telemetry_settings = cx.read(|cx| *settings::get::<TelemetrySettings>(cx));
 
         executor.clone().spawn(async move {
             // Generate a pair of asymmetric encryption keys. The public key will be used by the
@@ -1120,7 +1162,7 @@ impl Client {
             telemetry.report_mixpanel_event(
                 "authenticate with browser",
                 Default::default(),
-                metrics_enabled,
+                telemetry_settings,
             );
 
             Ok(Credentials {

crates/client/src/telemetry.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
+use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
     executor::Background,
@@ -9,7 +9,6 @@ use lazy_static::lazy_static;
 use parking_lot::Mutex;
 use serde::Serialize;
 use serde_json::json;
-use settings::TelemetrySettings;
 use std::{
     io::Write,
     mem,
@@ -246,9 +245,9 @@ impl Telemetry {
         self: &Arc<Self>,
         metrics_id: Option<String>,
         is_staff: bool,
-        telemetry_settings: TelemetrySettings,
+        cx: &AppContext,
     ) {
-        if !telemetry_settings.metrics() {
+        if !settings::get::<TelemetrySettings>(cx).metrics {
             return;
         }
 
@@ -290,7 +289,7 @@ impl Telemetry {
         event: ClickhouseEvent,
         telemetry_settings: TelemetrySettings,
     ) {
-        if !telemetry_settings.metrics() {
+        if !telemetry_settings.metrics {
             return;
         }
 
@@ -326,7 +325,7 @@ impl Telemetry {
         properties: Value,
         telemetry_settings: TelemetrySettings,
     ) {
-        if !telemetry_settings.metrics() {
+        if !telemetry_settings.metrics {
             return;
         }
 

crates/client/src/user.rs 🔗

@@ -5,7 +5,6 @@ use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
 use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
 use postage::{sink::Sink, watch};
 use rpc::proto::{RequestMessage, UsersResponse};
-use settings::Settings;
 use staff_mode::StaffMode;
 use std::sync::{Arc, Weak};
 use util::http::HttpClient;
@@ -144,11 +143,13 @@ impl UserStore {
                                 let fetch_metrics_id =
                                     client.request(proto::GetPrivateUserInfo {}).log_err();
                                 let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
-                                client.telemetry.set_authenticated_user_info(
-                                    info.as_ref().map(|info| info.metrics_id.clone()),
-                                    info.as_ref().map(|info| info.staff).unwrap_or(false),
-                                    cx.read(|cx| cx.global::<Settings>().telemetry()),
-                                );
+                                cx.read(|cx| {
+                                    client.telemetry.set_authenticated_user_info(
+                                        info.as_ref().map(|info| info.metrics_id.clone()),
+                                        info.as_ref().map(|info| info.staff).unwrap_or(false),
+                                        cx,
+                                    )
+                                });
 
                                 cx.update(|cx| {
                                     cx.update_default_global(|staff_mode: &mut StaffMode, _| {

crates/collab/Cargo.toml 🔗

@@ -51,7 +51,7 @@ tokio = { version = "1", features = ["full"] }
 tokio-tungstenite = "0.17"
 tonic = "0.6"
 tower = "0.4"
-toml = "0.5.8"
+toml.workspace = true
 tracing = "0.1.34"
 tracing-log = "0.1.3"
 tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }

crates/collab/src/tests.rs 🔗

@@ -19,7 +19,7 @@ use gpui::{
 use language::LanguageRegistry;
 use parking_lot::Mutex;
 use project::{Project, WorktreeId};
-use settings::Settings;
+use settings::SettingsStore;
 use std::{
     cell::{Ref, RefCell, RefMut},
     env,
@@ -30,7 +30,6 @@ use std::{
         Arc,
     },
 };
-use theme::ThemeRegistry;
 use util::http::FakeHttpClient;
 use workspace::Workspace;
 
@@ -102,7 +101,7 @@ impl TestServer {
 
     async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
         cx.update(|cx| {
-            cx.set_global(Settings::test(cx));
+            cx.set_global(SettingsStore::test(cx));
         });
 
         let http = FakeHttpClient::with_404_response();
@@ -191,7 +190,6 @@ impl TestServer {
             client: client.clone(),
             user_store: user_store.clone(),
             languages: Arc::new(LanguageRegistry::test()),
-            themes: ThemeRegistry::new((), cx.font_cache()),
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
             initialize_workspace: |_, _, _| unimplemented!(),
@@ -199,8 +197,12 @@ impl TestServer {
             background_actions: || &[],
         });
 
-        Project::init(&client);
         cx.update(|cx| {
+            theme::init((), cx);
+            Project::init(&client, cx);
+            client::init(&client, cx);
+            language::init(cx);
+            editor::init_settings(cx);
             workspace::init(app_state.clone(), cx);
             call::init(client.clone(), user_store.clone(), cx);
         });

crates/collab/src/tests/integration_tests.rs 🔗

@@ -18,6 +18,7 @@ use gpui::{
 };
 use indoc::indoc;
 use language::{
+    language_settings::{AllLanguageSettings, Formatter},
     tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
     LanguageConfig, OffsetRangeExt, Point, Rope,
 };
@@ -26,7 +27,7 @@ use lsp::LanguageServerId;
 use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath};
 use rand::prelude::*;
 use serde_json::json;
-use settings::{Formatter, Settings};
+use settings::SettingsStore;
 use std::{
     cell::{Cell, RefCell},
     env, future, mem,
@@ -1438,7 +1439,6 @@ async fn test_host_disconnect(
     cx_b: &mut TestAppContext,
     cx_c: &mut TestAppContext,
 ) {
-    cx_b.update(editor::init);
     deterministic.forbid_parking();
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
@@ -1448,6 +1448,8 @@ async fn test_host_disconnect(
         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
         .await;
 
+    cx_b.update(editor::init);
+
     client_a
         .fs
         .insert_tree(
@@ -1545,7 +1547,6 @@ async fn test_project_reconnect(
     cx_a: &mut TestAppContext,
     cx_b: &mut TestAppContext,
 ) {
-    cx_b.update(editor::init);
     deterministic.forbid_parking();
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
@@ -1554,6 +1555,8 @@ async fn test_project_reconnect(
         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
         .await;
 
+    cx_b.update(editor::init);
+
     client_a
         .fs
         .insert_tree(
@@ -4367,10 +4370,12 @@ async fn test_formatting_buffer(
     // Ensure buffer can be formatted using an external command. Notice how the
     // host's configuration is honored as opposed to using the guest's settings.
     cx_a.update(|cx| {
-        cx.update_global(|settings: &mut Settings, _| {
-            settings.editor_defaults.formatter = Some(Formatter::External {
-                command: "awk".to_string(),
-                arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
+        cx.update_global(|store: &mut SettingsStore, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
+                file.defaults.formatter = Some(Formatter::External {
+                    command: "awk".into(),
+                    arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
+                });
             });
         });
     });
@@ -5137,7 +5142,6 @@ async fn test_collaborating_with_code_actions(
     cx_b: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
-    cx_b.update(editor::init);
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
@@ -5146,6 +5150,8 @@ async fn test_collaborating_with_code_actions(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
+    cx_b.update(editor::init);
+
     // Set up a fake language server.
     let mut language = Language::new(
         LanguageConfig {
@@ -5350,7 +5356,6 @@ async fn test_collaborating_with_renames(
     cx_b: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
-    cx_b.update(editor::init);
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
@@ -5359,6 +5364,8 @@ async fn test_collaborating_with_renames(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
+    cx_b.update(editor::init);
+
     // Set up a fake language server.
     let mut language = Language::new(
         LanguageConfig {
@@ -5540,8 +5547,6 @@ async fn test_language_server_statuses(
     cx_b: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
-
-    cx_b.update(editor::init);
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
@@ -5550,6 +5555,8 @@ async fn test_language_server_statuses(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
+    cx_b.update(editor::init);
+
     // Set up a fake language server.
     let mut language = Language::new(
         LanguageConfig {
@@ -6257,8 +6264,6 @@ async fn test_basic_following(
     cx_d: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
 
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
@@ -6276,6 +6281,9 @@ async fn test_basic_following(
     let active_call_a = cx_a.read(ActiveCall::global);
     let active_call_b = cx_b.read(ActiveCall::global);
 
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
     client_a
         .fs
         .insert_tree(
@@ -6854,9 +6862,6 @@ async fn test_following_tab_order(
     cx_a: &mut TestAppContext,
     cx_b: &mut TestAppContext,
 ) {
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
-
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
@@ -6866,6 +6871,9 @@ async fn test_following_tab_order(
     let active_call_a = cx_a.read(ActiveCall::global);
     let active_call_b = cx_b.read(ActiveCall::global);
 
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
     client_a
         .fs
         .insert_tree(
@@ -6976,9 +6984,6 @@ async fn test_peers_following_each_other(
     cx_b: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
-
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
     let client_b = server.create_client(cx_b, "user_b").await;
@@ -6988,6 +6993,9 @@ async fn test_peers_following_each_other(
     let active_call_a = cx_a.read(ActiveCall::global);
     let active_call_b = cx_b.read(ActiveCall::global);
 
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
     // Client A shares a project.
     client_a
         .fs
@@ -7147,8 +7155,6 @@ async fn test_auto_unfollowing(
     cx_b: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
 
     // 2 clients connect to a server.
     let mut server = TestServer::start(&deterministic).await;
@@ -7160,6 +7166,9 @@ async fn test_auto_unfollowing(
     let active_call_a = cx_a.read(ActiveCall::global);
     let active_call_b = cx_b.read(ActiveCall::global);
 
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
     // Client A shares a project.
     client_a
         .fs
@@ -7314,8 +7323,6 @@ async fn test_peers_simultaneously_following_each_other(
     cx_b: &mut TestAppContext,
 ) {
     deterministic.forbid_parking();
-    cx_a.update(editor::init);
-    cx_b.update(editor::init);
 
     let mut server = TestServer::start(&deterministic).await;
     let client_a = server.create_client(cx_a, "user_a").await;
@@ -7325,6 +7332,9 @@ async fn test_peers_simultaneously_following_each_other(
         .await;
     let active_call_a = cx_a.read(ActiveCall::global);
 
+    cx_a.update(editor::init);
+    cx_b.update(editor::init);
+
     client_a.fs.insert_tree("/a", json!({})).await;
     let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
     let workspace_a = client_a.build_workspace(&project_a, cx_a);

crates/collab/src/tests/randomized_integration_tests.rs 🔗

@@ -21,7 +21,7 @@ use rand::{
     prelude::*,
 };
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::SettingsStore;
 use std::{
     env,
     ops::Range,
@@ -149,8 +149,9 @@ async fn test_random_collaboration(
 
     for (client, mut cx) in clients {
         cx.update(|cx| {
+            let store = cx.remove_global::<SettingsStore>();
             cx.clear_globals();
-            cx.set_global(Settings::test(cx));
+            cx.set_global(store);
             drop(client);
         });
     }

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -18,7 +18,6 @@ use gpui::{
     ViewContext, ViewHandle, WeakViewHandle,
 };
 use project::Project;
-use settings::Settings;
 use std::{ops::Range, sync::Arc};
 use theme::{AvatarStyle, Theme};
 use util::ResultExt;
@@ -70,7 +69,7 @@ impl View for CollabTitlebarItem {
         };
 
         let project = self.project.read(cx);
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
         let mut left_container = Flex::row();
         let mut right_container = Flex::row().align_children_center();
 
@@ -298,7 +297,7 @@ impl CollabTitlebarItem {
     }
 
     pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
         let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
         let item_style = theme.context_menu.item.disabled_style().clone();
         self.user_menu.update(cx, |user_menu, cx| {
@@ -866,7 +865,7 @@ impl CollabTitlebarItem {
     ) -> Option<AnyElement<Self>> {
         enum ConnectionStatusButton {}
 
-        let theme = &cx.global::<Settings>().theme.clone();
+        let theme = &theme::current(cx).clone();
         match status {
             client::Status::ConnectionError
             | client::Status::ConnectionLost

crates/collab_ui/src/contact_finder.rs 🔗

@@ -1,7 +1,6 @@
 use client::{ContactRequestStatus, User, UserStore};
 use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
 use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::Settings;
 use std::sync::Arc;
 use util::TryFutureExt;
 
@@ -98,7 +97,7 @@ impl PickerDelegate for ContactFinderDelegate {
         selected: bool,
         cx: &gpui::AppContext,
     ) -> AnyElement<Picker<Self>> {
-        let theme = &cx.global::<Settings>().theme;
+        let theme = &theme::current(cx);
         let user = &self.potential_contacts[ix];
         let request_status = self.user_store.read(cx).contact_request_status(user);
 

crates/collab_ui/src/contact_list.rs 🔗

@@ -14,7 +14,6 @@ use gpui::{
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::Project;
 use serde::Deserialize;
-use settings::Settings;
 use std::{mem, sync::Arc};
 use theme::IconButton;
 use workspace::Workspace;
@@ -192,7 +191,7 @@ impl ContactList {
         .detach();
 
         let list_state = ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
-            let theme = cx.global::<Settings>().theme.clone();
+            let theme = theme::current(cx).clone();
             let is_selected = this.selection == Some(ix);
             let current_project_id = this.project.read(cx).remote_id();
 
@@ -1313,7 +1312,7 @@ impl View for ContactList {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         enum AddContact {}
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
 
         Flex::column()
             .with_child(

crates/collab_ui/src/contacts_popover.rs 🔗

@@ -9,7 +9,6 @@ use gpui::{
 };
 use picker::PickerEvent;
 use project::Project;
-use settings::Settings;
 use workspace::Workspace;
 
 actions!(contacts_popover, [ToggleContactFinder]);
@@ -108,7 +107,7 @@ impl View for ContactsPopover {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
         let child = match &self.child {
             Child::ContactList(child) => ChildView::new(child, cx),
             Child::ContactFinder(child) => ChildView::new(child, cx),

crates/collab_ui/src/incoming_call_notification.rs 🔗

@@ -9,7 +9,6 @@ use gpui::{
     platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
     AnyElement, AppContext, Entity, View, ViewContext,
 };
-use settings::Settings;
 use util::ResultExt;
 use workspace::AppState;
 
@@ -26,7 +25,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
             if let Some(incoming_call) = incoming_call {
                 const PADDING: f32 = 16.;
                 let window_size = cx.read(|cx| {
-                    let theme = &cx.global::<Settings>().theme.incoming_call_notification;
+                    let theme = &theme::current(cx).incoming_call_notification;
                     vec2f(theme.window_width, theme.window_height)
                 });
 
@@ -106,7 +105,7 @@ impl IncomingCallNotification {
     }
 
     fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &cx.global::<Settings>().theme.incoming_call_notification;
+        let theme = &theme::current(cx).incoming_call_notification;
         let default_project = proto::ParticipantProject::default();
         let initial_project = self
             .call
@@ -170,10 +169,11 @@ impl IncomingCallNotification {
         enum Accept {}
         enum Decline {}
 
+        let theme = theme::current(cx);
         Flex::column()
             .with_child(
-                MouseEventHandler::<Accept, Self>::new(0, cx, |_, cx| {
-                    let theme = &cx.global::<Settings>().theme.incoming_call_notification;
+                MouseEventHandler::<Accept, Self>::new(0, cx, |_, _| {
+                    let theme = &theme.incoming_call_notification;
                     Label::new("Accept", theme.accept_button.text.clone())
                         .aligned()
                         .contained()
@@ -186,8 +186,8 @@ impl IncomingCallNotification {
                 .flex(1., true),
             )
             .with_child(
-                MouseEventHandler::<Decline, Self>::new(0, cx, |_, cx| {
-                    let theme = &cx.global::<Settings>().theme.incoming_call_notification;
+                MouseEventHandler::<Decline, Self>::new(0, cx, |_, _| {
+                    let theme = &theme.incoming_call_notification;
                     Label::new("Decline", theme.decline_button.text.clone())
                         .aligned()
                         .contained()
@@ -200,12 +200,7 @@ impl IncomingCallNotification {
                 .flex(1., true),
             )
             .constrained()
-            .with_width(
-                cx.global::<Settings>()
-                    .theme
-                    .incoming_call_notification
-                    .button_width,
-            )
+            .with_width(theme.incoming_call_notification.button_width)
             .into_any()
     }
 }
@@ -220,12 +215,7 @@ impl View for IncomingCallNotification {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let background = cx
-            .global::<Settings>()
-            .theme
-            .incoming_call_notification
-            .background;
-
+        let background = theme::current(cx).incoming_call_notification.background;
         Flex::row()
             .with_child(self.render_caller(cx))
             .with_child(self.render_buttons(cx))

crates/collab_ui/src/notifications.rs 🔗

@@ -4,7 +4,6 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     AnyElement, Element, View, ViewContext,
 };
-use settings::Settings;
 use std::sync::Arc;
 
 enum Dismiss {}
@@ -22,7 +21,7 @@ where
     F: 'static + Fn(&mut V, &mut ViewContext<V>),
     V: View,
 {
-    let theme = cx.global::<Settings>().theme.clone();
+    let theme = theme::current(cx).clone();
     let theme = &theme.contact_notification;
 
     Flex::column()

crates/collab_ui/src/project_shared_notification.rs 🔗

@@ -7,7 +7,6 @@ use gpui::{
     platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
     AppContext, Entity, View, ViewContext,
 };
-use settings::Settings;
 use std::sync::{Arc, Weak};
 use workspace::AppState;
 
@@ -22,7 +21,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
             worktree_root_names,
         } => {
             const PADDING: f32 = 16.;
-            let theme = &cx.global::<Settings>().theme.project_shared_notification;
+            let theme = &theme::current(cx).project_shared_notification;
             let window_size = vec2f(theme.window_width, theme.window_height);
 
             for screen in cx.platform().screens() {
@@ -109,7 +108,7 @@ impl ProjectSharedNotification {
     }
 
     fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &cx.global::<Settings>().theme.project_shared_notification;
+        let theme = &theme::current(cx).project_shared_notification;
         Flex::row()
             .with_children(self.owner.avatar.clone().map(|avatar| {
                 Image::from_data(avatar)
@@ -167,10 +166,11 @@ impl ProjectSharedNotification {
         enum Open {}
         enum Dismiss {}
 
+        let theme = theme::current(cx);
         Flex::column()
             .with_child(
-                MouseEventHandler::<Open, Self>::new(0, cx, |_, cx| {
-                    let theme = &cx.global::<Settings>().theme.project_shared_notification;
+                MouseEventHandler::<Open, Self>::new(0, cx, |_, _| {
+                    let theme = &theme.project_shared_notification;
                     Label::new("Open", theme.open_button.text.clone())
                         .aligned()
                         .contained()
@@ -181,8 +181,8 @@ impl ProjectSharedNotification {
                 .flex(1., true),
             )
             .with_child(
-                MouseEventHandler::<Dismiss, Self>::new(0, cx, |_, cx| {
-                    let theme = &cx.global::<Settings>().theme.project_shared_notification;
+                MouseEventHandler::<Dismiss, Self>::new(0, cx, |_, _| {
+                    let theme = &theme.project_shared_notification;
                     Label::new("Dismiss", theme.dismiss_button.text.clone())
                         .aligned()
                         .contained()
@@ -195,12 +195,7 @@ impl ProjectSharedNotification {
                 .flex(1., true),
             )
             .constrained()
-            .with_width(
-                cx.global::<Settings>()
-                    .theme
-                    .project_shared_notification
-                    .button_width,
-            )
+            .with_width(theme.project_shared_notification.button_width)
             .into_any()
     }
 }
@@ -215,11 +210,7 @@ impl View for ProjectSharedNotification {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
-        let background = cx
-            .global::<Settings>()
-            .theme
-            .project_shared_notification
-            .background;
+        let background = theme::current(cx).project_shared_notification.background;
         Flex::row()
             .with_child(self.render_owner(cx))
             .with_child(self.render_buttons(cx))

crates/collab_ui/src/sharing_status_indicator.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{
     platform::{Appearance, MouseButton},
     AnyElement, AppContext, Element, Entity, View, ViewContext,
 };
-use settings::Settings;
+use workspace::WorkspaceSettings;
 
 pub fn init(cx: &mut AppContext) {
     let active_call = ActiveCall::global(cx);
@@ -15,7 +15,9 @@ pub fn init(cx: &mut AppContext) {
     cx.observe(&active_call, move |call, cx| {
         if let Some(room) = call.read(cx).room() {
             if room.read(cx).is_screen_sharing() {
-                if status_indicator.is_none() && cx.global::<Settings>().show_call_status_icon {
+                if status_indicator.is_none()
+                    && settings::get::<WorkspaceSettings>(cx).show_call_status_icon
+                {
                     status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
                 }
             } else if let Some((window_id, _)) = status_indicator.take() {

crates/command_palette/Cargo.toml 🔗

@@ -23,6 +23,7 @@ workspace = { path = "../workspace" }
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
 serde_json.workspace = true
 workspace = { path = "../workspace", features = ["test-support"] }

crates/command_palette/src/command_palette.rs 🔗

@@ -5,7 +5,6 @@ use gpui::{
     ViewContext,
 };
 use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::Settings;
 use std::cmp;
 use util::ResultExt;
 use workspace::Workspace;
@@ -185,8 +184,7 @@ impl PickerDelegate for CommandPaletteDelegate {
     ) -> AnyElement<Picker<Self>> {
         let mat = &self.matches[ix];
         let command = &self.actions[mat.candidate_id];
-        let settings = cx.global::<Settings>();
-        let theme = &settings.theme;
+        let theme = theme::current(cx);
         let style = theme.picker.item.style_for(mouse_state, selected);
         let key_style = &theme.command_palette.key.style_for(mouse_state, selected);
         let keystroke_spacing = theme.command_palette.keystroke_spacing;
@@ -294,14 +292,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-        deterministic.forbid_parking();
-        let app_state = cx.update(AppState::test);
-
-        cx.update(|cx| {
-            editor::init(cx);
-            workspace::init(app_state.clone(), cx);
-            init(cx);
-        });
+        let app_state = init_test(cx);
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
         let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@@ -369,4 +360,16 @@ mod tests {
             assert!(palette.delegate().matches.is_empty())
         });
     }
+
+    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.update(|cx| {
+            let app_state = AppState::test(cx);
+            theme::init((), cx);
+            language::init(cx);
+            editor::init(cx);
+            workspace::init(app_state.clone(), cx);
+            init(cx);
+            app_state
+        })
+    }
 }

crates/context_menu/src/context_menu.rs 🔗

@@ -8,7 +8,6 @@ use gpui::{
     View, ViewContext,
 };
 use menu::*;
-use settings::Settings;
 use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
 
 pub fn init(cx: &mut AppContext) {
@@ -323,7 +322,7 @@ impl ContextMenu {
     }
 
     fn render_menu_for_measurement(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
-        let style = cx.global::<Settings>().theme.context_menu.clone();
+        let style = theme::current(cx).context_menu.clone();
         Flex::row()
             .with_child(
                 Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
@@ -403,7 +402,7 @@ impl ContextMenu {
         enum Menu {}
         enum MenuItem {}
 
-        let style = cx.global::<Settings>().theme.context_menu.clone();
+        let style = theme::current(cx).context_menu.clone();
 
         MouseEventHandler::<Menu, ContextMenu>::new(0, cx, |_, cx| {
             Flex::column()

crates/copilot/src/copilot.rs 🔗

@@ -10,6 +10,7 @@ use gpui::{
     actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle,
 };
 use language::{
+    language_settings::{all_language_settings, language_settings},
     point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
     ToPointUtf16,
 };
@@ -17,7 +18,7 @@ use log::{debug, error};
 use lsp::{LanguageServer, LanguageServerId};
 use node_runtime::NodeRuntime;
 use request::{LogMessage, StatusNotification};
-use settings::Settings;
+use settings::SettingsStore;
 use smol::{fs, io::BufReader, stream::StreamExt};
 use std::{
     ffi::OsString,
@@ -302,56 +303,34 @@ impl Copilot {
         node_runtime: Arc<NodeRuntime>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        cx.observe_global::<Settings, _>({
-            let http = http.clone();
-            let node_runtime = node_runtime.clone();
-            move |this, cx| {
-                if cx.global::<Settings>().features.copilot {
-                    if matches!(this.server, CopilotServer::Disabled) {
-                        let start_task = cx
-                            .spawn({
-                                let http = http.clone();
-                                let node_runtime = node_runtime.clone();
-                                move |this, cx| {
-                                    Self::start_language_server(http, node_runtime, this, cx)
-                                }
-                            })
-                            .shared();
-                        this.server = CopilotServer::Starting { task: start_task };
-                        cx.notify();
-                    }
-                } else {
-                    this.server = CopilotServer::Disabled;
-                    cx.notify();
-                }
-            }
-        })
-        .detach();
-
-        if cx.global::<Settings>().features.copilot {
-            let start_task = cx
-                .spawn({
-                    let http = http.clone();
-                    let node_runtime = node_runtime.clone();
-                    move |this, cx| async {
-                        Self::start_language_server(http, node_runtime, this, cx).await
-                    }
-                })
-                .shared();
+        let mut this = Self {
+            http,
+            node_runtime,
+            server: CopilotServer::Disabled,
+            buffers: Default::default(),
+        };
+        this.enable_or_disable_copilot(cx);
+        cx.observe_global::<SettingsStore, _>(move |this, cx| this.enable_or_disable_copilot(cx))
+            .detach();
+        this
+    }
 
-            Self {
-                http,
-                node_runtime,
-                server: CopilotServer::Starting { task: start_task },
-                buffers: Default::default(),
+    fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
+        let http = self.http.clone();
+        let node_runtime = self.node_runtime.clone();
+        if all_language_settings(cx).copilot_enabled(None, None) {
+            if matches!(self.server, CopilotServer::Disabled) {
+                let start_task = cx
+                    .spawn({
+                        move |this, cx| Self::start_language_server(http, node_runtime, this, cx)
+                    })
+                    .shared();
+                self.server = CopilotServer::Starting { task: start_task };
+                cx.notify();
             }
         } else {
-            Self {
-                http,
-                node_runtime,
-                server: CopilotServer::Disabled,
-                buffers: Default::default(),
-            }
+            self.server = CopilotServer::Disabled;
+            cx.notify();
         }
     }
 
@@ -805,13 +784,13 @@ impl Copilot {
         let snapshot = registered_buffer.report_changes(buffer, cx);
         let buffer = buffer.read(cx);
         let uri = registered_buffer.uri.clone();
-        let settings = cx.global::<Settings>();
         let position = position.to_point_utf16(buffer);
-        let language = buffer.language_at(position);
-        let language_name = language.map(|language| language.name());
-        let language_name = language_name.as_deref();
-        let tab_size = settings.tab_size(language_name);
-        let hard_tabs = settings.hard_tabs(language_name);
+        let settings = language_settings(
+            buffer.language_at(position).map(|l| l.name()).as_deref(),
+            cx,
+        );
+        let tab_size = settings.tab_size;
+        let hard_tabs = settings.hard_tabs;
         let relative_path = buffer
             .file()
             .map(|file| file.path().to_path_buf())

crates/copilot/src/sign_in.rs 🔗

@@ -6,7 +6,6 @@ use gpui::{
     AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
     ViewHandle,
 };
-use settings::Settings;
 use theme::ui::modal;
 
 #[derive(PartialEq, Eq, Debug, Clone)]
@@ -68,7 +67,7 @@ fn create_copilot_auth_window(
     cx: &mut AppContext,
     status: &Status,
 ) -> ViewHandle<CopilotCodeVerification> {
-    let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
+    let window_size = theme::current(cx).copilot.modal.dimensions();
     let window_options = WindowOptions {
         bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
         titlebar: None,
@@ -338,7 +337,7 @@ impl View for CopilotCodeVerification {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         enum ConnectModal {}
 
-        let style = cx.global::<Settings>().theme.clone();
+        let style = theme::current(cx).clone();
 
         modal::<ConnectModal, _, _, _, _>(
             "Connect Copilot to Zed",

crates/copilot_button/Cargo.toml 🔗

@@ -12,8 +12,10 @@ doctest = false
 assets = { path = "../assets" }
 copilot = { path = "../copilot" }
 editor = { path = "../editor" }
+fs = { path = "../fs" }
 context_menu = { path = "../context_menu" }
 gpui = { path = "../gpui" }
+language = { path = "../language" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }
 util = { path = "../util" }

crates/copilot_button/src/copilot_button.rs 🔗

@@ -2,13 +2,15 @@ use anyhow::Result;
 use context_menu::{ContextMenu, ContextMenuItem};
 use copilot::{Copilot, SignOut, Status};
 use editor::{scroll::autoscroll::Autoscroll, Editor};
+use fs::Fs;
 use gpui::{
     elements::*,
     platform::{CursorStyle, MouseButton},
     AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
-use settings::{settings_file::SettingsFile, Settings};
+use language::language_settings::{self, all_language_settings, AllLanguageSettings};
+use settings::{update_settings_file, SettingsStore};
 use std::{path::Path, sync::Arc};
 use util::{paths, ResultExt};
 use workspace::{
@@ -26,6 +28,7 @@ pub struct CopilotButton {
     editor_enabled: Option<bool>,
     language: Option<Arc<str>>,
     path: Option<Arc<Path>>,
+    fs: Arc<dyn Fs>,
 }
 
 impl Entity for CopilotButton {
@@ -38,13 +41,12 @@ impl View for CopilotButton {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let settings = cx.global::<Settings>();
-
-        if !settings.features.copilot {
+        let all_language_settings = &all_language_settings(cx);
+        if !all_language_settings.copilot.feature_enabled {
             return Empty::new().into_any();
         }
 
-        let theme = settings.theme.clone();
+        let theme = theme::current(cx).clone();
         let active = self.popup_menu.read(cx).visible();
         let Some(copilot) = Copilot::global(cx) else {
             return Empty::new().into_any();
@@ -53,7 +55,7 @@ impl View for CopilotButton {
 
         let enabled = self
             .editor_enabled
-            .unwrap_or(settings.show_copilot_suggestions(None, None));
+            .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
 
         Stack::new()
             .with_child(
@@ -143,7 +145,7 @@ impl View for CopilotButton {
 }
 
 impl CopilotButton {
-    pub fn new(cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
         let button_view_id = cx.view_id();
         let menu = cx.add_view(|cx| {
             let mut menu = ContextMenu::new(button_view_id, cx);
@@ -155,7 +157,7 @@ impl CopilotButton {
 
         Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
 
-        cx.observe_global::<Settings, _>(move |_, cx| cx.notify())
+        cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify())
             .detach();
 
         Self {
@@ -164,17 +166,19 @@ impl CopilotButton {
             editor_enabled: None,
             language: None,
             path: None,
+            fs,
         }
     }
 
     pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
         let mut menu_options = Vec::with_capacity(2);
+        let fs = self.fs.clone();
 
         menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
             initiate_sign_in(cx)
         }));
-        menu_options.push(ContextMenuItem::handler("Disable Copilot", |cx| {
-            hide_copilot(cx)
+        menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| {
+            hide_copilot(fs.clone(), cx)
         }));
 
         self.popup_menu.update(cx, |menu, cx| {
@@ -188,22 +192,26 @@ impl CopilotButton {
     }
 
     pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
-        let settings = cx.global::<Settings>();
-
+        let fs = self.fs.clone();
         let mut menu_options = Vec::with_capacity(8);
 
         if let Some(language) = self.language.clone() {
-            let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref()));
+            let fs = fs.clone();
+            let language_enabled =
+                language_settings::language_settings(Some(language.as_ref()), cx)
+                    .show_copilot_suggestions;
             menu_options.push(ContextMenuItem::handler(
                 format!(
                     "{} Suggestions for {}",
                     if language_enabled { "Hide" } else { "Show" },
                     language
                 ),
-                move |cx| toggle_copilot_for_language(language.clone(), cx),
+                move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
             ));
         }
 
+        let settings = settings::get::<AllLanguageSettings>(cx);
+
         if let Some(path) = self.path.as_ref() {
             let path_enabled = settings.copilot_enabled_for_path(path);
             let path = path.clone();
@@ -228,19 +236,19 @@ impl CopilotButton {
             ));
         }
 
-        let globally_enabled = cx.global::<Settings>().features.copilot;
+        let globally_enabled = settings.copilot_enabled(None, None);
         menu_options.push(ContextMenuItem::handler(
             if globally_enabled {
                 "Hide Suggestions for All Files"
             } else {
                 "Show Suggestions for All Files"
             },
-            |cx| toggle_copilot_globally(cx),
+            move |cx| toggle_copilot_globally(fs.clone(), cx),
         ));
 
         menu_options.push(ContextMenuItem::Separator);
 
-        let icon_style = settings.theme.copilot.out_link_icon.clone();
+        let icon_style = theme::current(cx).copilot.out_link_icon.clone();
         menu_options.push(ContextMenuItem::action(
             move |state: &mut MouseState, style: &theme::ContextMenuItem| {
                 Flex::row()
@@ -266,22 +274,19 @@ impl CopilotButton {
 
     pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
         let editor = editor.read(cx);
-
         let snapshot = editor.buffer().read(cx).snapshot(cx);
-        let settings = cx.global::<Settings>();
         let suggestion_anchor = editor.selections.newest_anchor().start;
-
         let language_name = snapshot
             .language_at(suggestion_anchor)
             .map(|language| language.name());
-        let path = snapshot
-            .file_at(suggestion_anchor)
-            .map(|file| file.path().clone());
+        let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
 
-        self.editor_enabled =
-            Some(settings.show_copilot_suggestions(language_name.as_deref(), path.as_deref()));
+        self.editor_enabled = Some(
+            all_language_settings(cx)
+                .copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())),
+        );
         self.language = language_name;
-        self.path = path;
+        self.path = path.cloned();
 
         cx.notify()
     }
@@ -310,7 +315,7 @@ async fn configure_disabled_globs(
     let settings_editor = workspace
         .update(&mut cx, |_, cx| {
             create_and_open_local_file(&paths::SETTINGS, cx, || {
-                Settings::initial_user_settings_content(&assets::Assets)
+                settings::initial_user_settings_content(&assets::Assets)
                     .as_ref()
                     .into()
             })
@@ -322,10 +327,12 @@ async fn configure_disabled_globs(
     settings_editor.downgrade().update(&mut cx, |item, cx| {
         let text = item.buffer().read(cx).snapshot(cx).text();
 
-        let edits = SettingsFile::update_unsaved(&text, cx, |file| {
+        let settings = cx.global::<SettingsStore>();
+        let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
             let copilot = file.copilot.get_or_insert_with(Default::default);
             let globs = copilot.disabled_globs.get_or_insert_with(|| {
-                cx.global::<Settings>()
+                settings
+                    .get::<AllLanguageSettings>(None)
                     .copilot
                     .disabled_globs
                     .clone()
@@ -356,32 +363,26 @@ async fn configure_disabled_globs(
     anyhow::Ok(())
 }
 
-fn toggle_copilot_globally(cx: &mut AppContext) {
-    let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None, None);
-    SettingsFile::update(cx, move |file_contents| {
-        file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
+fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None);
+    update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+        file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
     });
 }
 
-fn toggle_copilot_for_language(language: Arc<str>, cx: &mut AppContext) {
-    let show_copilot_suggestions = cx
-        .global::<Settings>()
-        .show_copilot_suggestions(Some(&language), None);
-
-    SettingsFile::update(cx, move |file_contents| {
-        file_contents.languages.insert(
-            language,
-            settings::EditorSettings {
-                show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
-                ..Default::default()
-            },
-        );
+fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None);
+    update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+        file.languages
+            .entry(language)
+            .or_default()
+            .show_copilot_suggestions = Some(!show_copilot_suggestions);
     });
 }
 
-fn hide_copilot(cx: &mut AppContext) {
-    SettingsFile::update(cx, move |file_contents| {
-        file_contents.features.copilot = Some(false)
+fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+        file.features.get_or_insert(Default::default()).copilot = Some(false);
     });
 }
 

crates/diagnostics/Cargo.toml 🔗

@@ -31,6 +31,7 @@ language = { path = "../language", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
+theme = { path = "../theme", features = ["test-support"] }
 
 serde_json.workspace = true
 unindent.workspace = true

crates/diagnostics/src/diagnostics.rs 🔗

@@ -20,7 +20,6 @@ use language::{
 use lsp::LanguageServerId;
 use project::{DiagnosticSummary, Project, ProjectPath};
 use serde_json::json;
-use settings::Settings;
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -30,6 +29,7 @@ use std::{
     path::PathBuf,
     sync::Arc,
 };
+use theme::ThemeSettings;
 use util::TryFutureExt;
 use workspace::{
     item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
@@ -89,7 +89,7 @@ impl View for ProjectDiagnosticsEditor {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         if self.path_states.is_empty() {
-            let theme = &cx.global::<Settings>().theme.project_diagnostics;
+            let theme = &theme::current(cx).project_diagnostics;
             Label::new("No problems in workspace", theme.empty_message.clone())
                 .aligned()
                 .contained()
@@ -537,7 +537,7 @@ impl Item for ProjectDiagnosticsEditor {
         render_summary(
             &self.summary,
             &style.label.text,
-            &cx.global::<Settings>().theme.project_diagnostics,
+            &theme::current(cx).project_diagnostics,
         )
     }
 
@@ -679,10 +679,10 @@ impl Item for ProjectDiagnosticsEditor {
 fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
     let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
     Arc::new(move |cx| {
-        let settings = cx.global::<Settings>();
+        let settings = settings::get::<ThemeSettings>(cx);
         let theme = &settings.theme.editor;
         let style = theme.diagnostic_header.clone();
-        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
+        let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
         let icon_width = cx.em_width * style.icon_width_factor;
         let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
             Svg::new("icons/circle_x_mark_12.svg")
@@ -818,33 +818,35 @@ mod tests {
     use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
     use project::FakeFs;
     use serde_json::json;
+    use settings::SettingsStore;
     use unindent::Unindent as _;
 
     #[gpui::test]
     async fn test_diagnostics(cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/test",
             json!({
                 "consts.rs": "
-                        const a: i32 = 'a';
-                        const b: i32 = c;
-                    "
+                    const a: i32 = 'a';
+                    const b: i32 = c;
+                "
                 .unindent(),
 
                 "main.rs": "
-                        fn main() {
-                            let x = vec![];
-                            let y = vec![];
-                            a(x);
-                            b(y);
-                            // comment 1
-                            // comment 2
-                            c(y);
-                            d(x);
-                        }
-                    "
+                    fn main() {
+                        let x = vec![];
+                        let y = vec![];
+                        a(x);
+                        b(y);
+                        // comment 1
+                        // comment 2
+                        c(y);
+                        d(x);
+                    }
+                "
                 .unindent(),
             }),
         )
@@ -1225,7 +1227,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/test",
@@ -1489,6 +1492,16 @@ mod tests {
         });
     }
 
+    fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            language::init(cx);
+            client::init_settings(cx);
+            workspace::init_settings(cx);
+        });
+    }
+
     fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
         editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(cx);

crates/diagnostics/src/items.rs 🔗

@@ -7,7 +7,6 @@ use gpui::{
 };
 use language::Diagnostic;
 use lsp::LanguageServerId;
-use settings::Settings;
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
 use crate::ProjectDiagnosticsEditor;
@@ -92,13 +91,12 @@ impl View for DiagnosticIndicator {
         enum Summary {}
         enum Message {}
 
-        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+        let tooltip_style = theme::current(cx).tooltip.clone();
         let in_progress = !self.in_progress_checks.is_empty();
         let mut element = Flex::row().with_child(
             MouseEventHandler::<Summary, _>::new(0, cx, |state, cx| {
-                let style = cx
-                    .global::<Settings>()
-                    .theme
+                let theme = theme::current(cx);
+                let style = theme
                     .workspace
                     .status_bar
                     .diagnostic_summary
@@ -184,7 +182,7 @@ impl View for DiagnosticIndicator {
             .into_any(),
         );
 
-        let style = &cx.global::<Settings>().theme.workspace.status_bar;
+        let style = &theme::current(cx).workspace.status_bar;
         let item_spacing = style.item_spacing;
 
         if in_progress {

crates/editor/Cargo.toml 🔗

@@ -49,6 +49,7 @@ workspace = { path = "../workspace" }
 aho-corasick = "0.7"
 anyhow.workspace = true
 futures.workspace = true
+glob.workspace = true
 indoc = "1.0.4"
 itertools = "0.10"
 lazy_static.workspace = true
@@ -58,6 +59,7 @@ parking_lot.workspace = true
 postage.workspace = true
 pulldown-cmark = { version = "0.9.2", default-features = false }
 rand = { workspace = true, optional = true }
+schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 smallvec.workspace = true
@@ -1,8 +1,8 @@
-use std::time::Duration;
-
+use crate::EditorSettings;
 use gpui::{Entity, ModelContext};
-use settings::Settings;
+use settings::SettingsStore;
 use smol::Timer;
+use std::time::Duration;
 
 pub struct BlinkManager {
     blink_interval: Duration,
@@ -15,8 +15,8 @@ pub struct BlinkManager {
 
 impl BlinkManager {
     pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
-        cx.observe_global::<Settings, _>(move |this, cx| {
-            // Make sure we blink the cursors if the setting is re-enabled
+        // Make sure we blink the cursors if the setting is re-enabled
+        cx.observe_global::<SettingsStore, _>(move |this, cx| {
             this.blink_cursors(this.blink_epoch, cx)
         })
         .detach();
@@ -64,7 +64,7 @@ impl BlinkManager {
     }
 
     fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
-        if cx.global::<Settings>().cursor_blink {
+        if settings::get::<EditorSettings>(cx).cursor_blink {
             if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
                 self.visible = !self.visible;
                 cx.notify();

crates/editor/src/display_map.rs 🔗

@@ -13,8 +13,9 @@ use gpui::{
     fonts::{FontId, HighlightStyle},
     Entity, ModelContext, ModelHandle,
 };
-use language::{OffsetUtf16, Point, Subscription as BufferSubscription};
-use settings::Settings;
+use language::{
+    language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
+};
 use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
 pub use suggestion_map::Suggestion;
 use suggestion_map::SuggestionMap;
@@ -276,8 +277,7 @@ impl DisplayMap {
             .as_singleton()
             .and_then(|buffer| buffer.read(cx).language())
             .map(|language| language.name());
-
-        cx.global::<Settings>().tab_size(language_name.as_deref())
+        language_settings(language_name.as_deref(), cx).tab_size
     }
 
     #[cfg(test)]
@@ -844,8 +844,12 @@ pub mod tests {
     use super::*;
     use crate::{movement, test::marked_display_snapshot};
     use gpui::{color::Color, elements::*, test::observe, AppContext};
-    use language::{Buffer, Language, LanguageConfig, SelectionGoal};
+    use language::{
+        language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
+        Buffer, Language, LanguageConfig, SelectionGoal,
+    };
     use rand::{prelude::*, Rng};
+    use settings::SettingsStore;
     use smol::stream::StreamExt;
     use std::{env, sync::Arc};
     use theme::SyntaxTheme;
@@ -882,9 +886,7 @@ pub mod tests {
         log::info!("wrap width: {:?}", wrap_width);
 
         cx.update(|cx| {
-            let mut settings = Settings::test(cx);
-            settings.editor_overrides.tab_size = NonZeroU32::new(tab_size);
-            cx.set_global(settings)
+            init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
         });
 
         let buffer = cx.update(|cx| {
@@ -939,9 +941,11 @@ pub mod tests {
                     tab_size = *tab_sizes.choose(&mut rng).unwrap();
                     log::info!("setting tab size to {:?}", tab_size);
                     cx.update(|cx| {
-                        let mut settings = Settings::test(cx);
-                        settings.editor_overrides.tab_size = NonZeroU32::new(tab_size);
-                        cx.set_global(settings)
+                        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                            store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                                s.defaults.tab_size = NonZeroU32::new(tab_size);
+                            });
+                        });
                     });
                 }
                 30..=44 => {
@@ -1119,7 +1123,7 @@ pub mod tests {
     #[gpui::test(retries = 5)]
     fn test_soft_wraps(cx: &mut AppContext) {
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
-        cx.foreground().forbid_parking();
+        init_test(cx, |_| {});
 
         let font_cache = cx.font_cache();
 
@@ -1131,7 +1135,6 @@ pub mod tests {
             .unwrap();
         let font_size = 12.0;
         let wrap_width = Some(64.);
-        cx.set_global(Settings::test(cx));
 
         let text = "one two three four five\nsix seven eight";
         let buffer = MultiBuffer::build_simple(text, cx);
@@ -1211,7 +1214,8 @@ pub mod tests {
 
     #[gpui::test]
     fn test_text_chunks(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx, |_| {});
+
         let text = sample_text(6, 6, 'a');
         let buffer = MultiBuffer::build_simple(&text, cx);
         let family_id = cx
@@ -1225,6 +1229,7 @@ pub mod tests {
         let font_size = 14.0;
         let map =
             cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
+
         buffer.update(cx, |buffer, cx| {
             buffer.edit(
                 vec![
@@ -1289,11 +1294,8 @@ pub mod tests {
             .unwrap(),
         );
         language.set_theme(&theme);
-        cx.update(|cx| {
-            let mut settings = Settings::test(cx);
-            settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
-            cx.set_global(settings);
-        });
+
+        cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
 
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
@@ -1382,7 +1384,7 @@ pub mod tests {
         );
         language.set_theme(&theme);
 
-        cx.update(|cx| cx.set_global(Settings::test(cx)));
+        cx.update(|cx| init_test(cx, |_| {}));
 
         let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
         buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
@@ -1429,9 +1431,8 @@ pub mod tests {
 
     #[gpui::test]
     async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
-        cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
+        cx.update(|cx| init_test(cx, |_| {}));
 
-        cx.update(|cx| cx.set_global(Settings::test(cx)));
         let theme = SyntaxTheme::new(vec![
             ("operator".to_string(), Color::red().into()),
             ("string".to_string(), Color::green().into()),
@@ -1510,7 +1511,8 @@ pub mod tests {
 
     #[gpui::test]
     fn test_clip_point(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx, |_| {});
+
         fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
             let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
 
@@ -1559,7 +1561,7 @@ pub mod tests {
 
     #[gpui::test]
     fn test_clip_at_line_ends(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx, |_| {});
 
         fn assert(text: &str, cx: &mut gpui::AppContext) {
             let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
@@ -1578,7 +1580,8 @@ pub mod tests {
 
     #[gpui::test]
     fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx, |_| {});
+
         let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
         let buffer = MultiBuffer::build_simple(text, cx);
         let font_cache = cx.font_cache();
@@ -1639,7 +1642,8 @@ pub mod tests {
 
     #[gpui::test]
     fn test_max_point(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx, |_| {});
+
         let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
         let font_cache = cx.font_cache();
         let family_id = font_cache
@@ -1718,4 +1722,13 @@ pub mod tests {
         }
         chunks
     }
+
+    fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+        cx.foreground().forbid_parking();
+        cx.set_global(SettingsStore::test(cx));
+        language::init(cx);
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, f);
+        });
+    }
 }

crates/editor/src/display_map/block_map.rs 🔗

@@ -993,7 +993,7 @@ mod tests {
     use crate::multi_buffer::MultiBuffer;
     use gpui::{elements::Empty, Element};
     use rand::prelude::*;
-    use settings::Settings;
+    use settings::SettingsStore;
     use std::env;
     use util::RandomCharIter;
 
@@ -1013,7 +1013,7 @@ mod tests {
 
     #[gpui::test]
     fn test_basic_blocks(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
 
         let family_id = cx
             .font_cache()
@@ -1189,7 +1189,7 @@ mod tests {
 
     #[gpui::test]
     fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
 
         let family_id = cx
             .font_cache()
@@ -1239,7 +1239,7 @@ mod tests {
 
     #[gpui::test(iterations = 100)]
     fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
 
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
@@ -1647,6 +1647,11 @@ mod tests {
         }
     }
 
+    fn init_test(cx: &mut gpui::AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+    }
+
     impl TransformBlock {
         fn as_custom(&self) -> Option<&Block> {
             match self {

crates/editor/src/display_map/fold_map.rs 🔗

@@ -1204,7 +1204,7 @@ mod tests {
     use crate::{MultiBuffer, ToPoint};
     use collections::HashSet;
     use rand::prelude::*;
-    use settings::Settings;
+    use settings::SettingsStore;
     use std::{cmp::Reverse, env, mem, sync::Arc};
     use sum_tree::TreeMap;
     use util::test::sample_text;
@@ -1213,7 +1213,7 @@ mod tests {
 
     #[gpui::test]
     fn test_basic_folds(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
         let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1286,7 +1286,7 @@ mod tests {
 
     #[gpui::test]
     fn test_adjacent_folds(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
         let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1349,7 +1349,7 @@ mod tests {
 
     #[gpui::test]
     fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
         let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
         let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1400,7 +1400,7 @@ mod tests {
 
     #[gpui::test(iterations = 100)]
     fn test_random_folds(cx: &mut gpui::AppContext, mut rng: StdRng) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
             .unwrap_or(10);
@@ -1676,6 +1676,10 @@ mod tests {
         assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]);
     }
 
+    fn init_test(cx: &mut gpui::AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+    }
+
     impl FoldMap {
         fn merged_fold_ranges(&self) -> Vec<Range<usize>> {
             let buffer = self.buffer.lock().clone();

crates/editor/src/display_map/suggestion_map.rs 🔗

@@ -578,7 +578,7 @@ mod tests {
     use crate::{display_map::fold_map::FoldMap, MultiBuffer};
     use gpui::AppContext;
     use rand::{prelude::StdRng, Rng};
-    use settings::Settings;
+    use settings::SettingsStore;
     use std::{
         env,
         ops::{Bound, RangeBounds},
@@ -631,7 +631,8 @@ mod tests {
 
     #[gpui::test(iterations = 100)]
     fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
+
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
             .unwrap_or(10);
@@ -834,6 +835,11 @@ mod tests {
         }
     }
 
+    fn init_test(cx: &mut AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+    }
+
     impl SuggestionMap {
         pub fn randomly_mutate(
             &self,

crates/editor/src/display_map/wrap_map.rs 🔗

@@ -1043,16 +1043,16 @@ mod tests {
     };
     use gpui::test::observe;
     use rand::prelude::*;
-    use settings::Settings;
+    use settings::SettingsStore;
     use smol::stream::StreamExt;
     use std::{cmp, env, num::NonZeroU32};
     use text::Rope;
 
     #[gpui::test(iterations = 100)]
     async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
-        cx.update(|cx| cx.set_global(Settings::test(cx)));
+        init_test(cx);
+
         cx.foreground().set_block_on_ticks(0..=50);
-        cx.foreground().forbid_parking();
         let operations = env::var("OPERATIONS")
             .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
             .unwrap_or(10);
@@ -1287,6 +1287,14 @@ mod tests {
         wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
     }
 
+    fn init_test(cx: &mut gpui::TestAppContext) {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+        });
+    }
+
     fn wrap_text(
         unwrapped_text: &str,
         wrap_width: Option<f32>,

crates/editor/src/editor.rs 🔗

@@ -1,5 +1,6 @@
 mod blink_manager;
 pub mod display_map;
+mod editor_settings;
 mod element;
 
 mod git;
@@ -22,12 +23,13 @@ pub mod test;
 use aho_corasick::AhoCorasick;
 use anyhow::{anyhow, Result};
 use blink_manager::BlinkManager;
-use client::ClickhouseEvent;
+use client::{ClickhouseEvent, TelemetrySettings};
 use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use copilot::Copilot;
 pub use display_map::DisplayPoint;
 use display_map::*;
+pub use editor_settings::EditorSettings;
 pub use element::*;
 use futures::FutureExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
@@ -51,6 +53,7 @@ pub use items::MAX_TAB_TITLE_LEN;
 use itertools::Itertools;
 pub use language::{char_kind, CharKind};
 use language::{
+    language_settings::{self, all_language_settings},
     AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
     Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
     OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
@@ -70,7 +73,7 @@ use scroll::{
 };
 use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
 use serde::{Deserialize, Serialize};
-use settings::Settings;
+use settings::SettingsStore;
 use smallvec::SmallVec;
 use snippet::Snippet;
 use std::{
@@ -85,7 +88,7 @@ use std::{
     time::{Duration, Instant},
 };
 pub use sum_tree::Bias;
-use theme::{DiagnosticStyle, Theme};
+use theme::{DiagnosticStyle, Theme, ThemeSettings};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{ItemNavHistory, ViewId, Workspace};
 
@@ -286,7 +289,12 @@ pub enum Direction {
     Next,
 }
 
+pub fn init_settings(cx: &mut AppContext) {
+    settings::register::<EditorSettings>(cx);
+}
+
 pub fn init(cx: &mut AppContext) {
+    init_settings(cx);
     cx.add_action(Editor::new_file);
     cx.add_action(Editor::cancel);
     cx.add_action(Editor::newline);
@@ -436,7 +444,7 @@ pub enum EditorMode {
     Full,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub enum SoftWrap {
     None,
     EditorWidth,
@@ -471,7 +479,7 @@ pub struct Editor {
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
     ime_transaction: Option<TransactionId>,
     active_diagnostics: Option<ActiveDiagnosticGroup>,
-    soft_wrap_mode_override: Option<settings::SoftWrap>,
+    soft_wrap_mode_override: Option<language_settings::SoftWrap>,
     get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
     override_text_style: Option<Box<OverrideTextStyle>>,
     project: Option<ModelHandle<Project>>,
@@ -1238,8 +1246,8 @@ impl Editor {
     ) -> Self {
         let editor_view_id = cx.view_id();
         let display_map = cx.add_model(|cx| {
-            let settings = cx.global::<Settings>();
-            let style = build_style(&*settings, get_field_editor_theme.as_deref(), None, cx);
+            let settings = settings::get::<ThemeSettings>(cx);
+            let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx);
             DisplayMap::new(
                 buffer.clone(),
                 style.text.font_id,
@@ -1256,7 +1264,7 @@ impl Editor {
         let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
 
         let soft_wrap_mode_override =
-            (mode == EditorMode::SingleLine).then(|| settings::SoftWrap::None);
+            (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
 
         let mut project_subscription = None;
         if mode == EditorMode::Full && buffer.read(cx).is_singleton() {
@@ -1320,7 +1328,7 @@ impl Editor {
                 cx.subscribe(&buffer, Self::on_buffer_event),
                 cx.observe(&display_map, Self::on_display_map_changed),
                 cx.observe(&blink_manager, |_, _, cx| cx.notify()),
-                cx.observe_global::<Settings, _>(Self::settings_changed),
+                cx.observe_global::<SettingsStore, _>(Self::settings_changed),
             ],
         };
 
@@ -1419,7 +1427,7 @@ impl Editor {
 
     fn style(&self, cx: &AppContext) -> EditorStyle {
         build_style(
-            cx.global::<Settings>(),
+            settings::get::<ThemeSettings>(cx),
             self.get_field_editor_theme.as_deref(),
             self.override_text_style.as_deref(),
             cx,
@@ -2377,7 +2385,7 @@ impl Editor {
     }
 
     fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
-        if !cx.global::<Settings>().show_completions_on_input {
+        if !settings::get::<EditorSettings>(cx).show_completions_on_input {
             return;
         }
 
@@ -3144,17 +3152,12 @@ impl Editor {
         snapshot: &MultiBufferSnapshot,
         cx: &mut ViewContext<Self>,
     ) -> bool {
-        let settings = cx.global::<Settings>();
-
-        let path = snapshot.file_at(location).map(|file| file.path());
+        let path = snapshot.file_at(location).map(|file| file.path().as_ref());
         let language_name = snapshot
             .language_at(location)
             .map(|language| language.name());
-        if !settings.show_copilot_suggestions(language_name.as_deref(), path.map(|p| p.as_ref())) {
-            return false;
-        }
-
-        true
+        let settings = all_language_settings(cx);
+        settings.copilot_enabled(language_name.as_deref(), path)
     }
 
     fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
@@ -3455,12 +3458,9 @@ impl Editor {
                         {
                             let indent_size =
                                 buffer.indent_size_for_line(line_buffer_range.start.row);
-                            let language_name = buffer
-                                .language_at(line_buffer_range.start)
-                                .map(|language| language.name());
                             let indent_len = match indent_size.kind {
                                 IndentKind::Space => {
-                                    cx.global::<Settings>().tab_size(language_name.as_deref())
+                                    buffer.settings_at(line_buffer_range.start, cx).tab_size
                                 }
                                 IndentKind::Tab => NonZeroU32::new(1).unwrap(),
                             };
@@ -3572,12 +3572,11 @@ impl Editor {
             }
 
             // Otherwise, insert a hard or soft tab.
-            let settings = cx.global::<Settings>();
-            let language_name = buffer.language_at(cursor, cx).map(|l| l.name());
-            let tab_size = if settings.hard_tabs(language_name.as_deref()) {
+            let settings = buffer.settings_at(cursor, cx);
+            let tab_size = if settings.hard_tabs {
                 IndentSize::tab()
             } else {
-                let tab_size = settings.tab_size(language_name.as_deref()).get();
+                let tab_size = settings.tab_size.get();
                 let char_column = snapshot
                     .text_for_range(Point::new(cursor.row, 0)..cursor)
                     .flat_map(str::chars)
@@ -3630,10 +3629,9 @@ impl Editor {
         delta_for_start_row: u32,
         cx: &AppContext,
     ) -> u32 {
-        let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
-        let settings = cx.global::<Settings>();
-        let tab_size = settings.tab_size(language_name.as_deref()).get();
-        let indent_kind = if settings.hard_tabs(language_name.as_deref()) {
+        let settings = buffer.settings_at(selection.start, cx);
+        let tab_size = settings.tab_size.get();
+        let indent_kind = if settings.hard_tabs {
             IndentKind::Tab
         } else {
             IndentKind::Space
@@ -3702,11 +3700,8 @@ impl Editor {
             let buffer = self.buffer.read(cx);
             let snapshot = buffer.snapshot(cx);
             for selection in &selections {
-                let language_name = buffer.language_at(selection.start, cx).map(|l| l.name());
-                let tab_size = cx
-                    .global::<Settings>()
-                    .tab_size(language_name.as_deref())
-                    .get();
+                let settings = buffer.settings_at(selection.start, cx);
+                let tab_size = settings.tab_size.get();
                 let mut rows = selection.spanned_rows(false, &display_map);
 
                 // Avoid re-outdenting a row that has already been outdented by a
@@ -6467,27 +6462,24 @@ impl Editor {
     }
 
     pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
-        let language_name = self
-            .buffer
-            .read(cx)
-            .as_singleton()
-            .and_then(|singleton_buffer| singleton_buffer.read(cx).language())
-            .map(|l| l.name());
-
-        let settings = cx.global::<Settings>();
+        let settings = self.buffer.read(cx).settings_at(0, cx);
         let mode = self
             .soft_wrap_mode_override
-            .unwrap_or_else(|| settings.soft_wrap(language_name.as_deref()));
+            .unwrap_or_else(|| settings.soft_wrap);
         match mode {
-            settings::SoftWrap::None => SoftWrap::None,
-            settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
-            settings::SoftWrap::PreferredLineLength => {
-                SoftWrap::Column(settings.preferred_line_length(language_name.as_deref()))
+            language_settings::SoftWrap::None => SoftWrap::None,
+            language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
+            language_settings::SoftWrap::PreferredLineLength => {
+                SoftWrap::Column(settings.preferred_line_length)
             }
         }
     }
 
-    pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext<Self>) {
+    pub fn set_soft_wrap_mode(
+        &mut self,
+        mode: language_settings::SoftWrap,
+        cx: &mut ViewContext<Self>,
+    ) {
         self.soft_wrap_mode_override = Some(mode);
         cx.notify();
     }
@@ -6502,8 +6494,8 @@ impl Editor {
             self.soft_wrap_mode_override.take();
         } else {
             let soft_wrap = match self.soft_wrap_mode(cx) {
-                SoftWrap::None => settings::SoftWrap::EditorWidth,
-                SoftWrap::EditorWidth | SoftWrap::Column(_) => settings::SoftWrap::None,
+                SoftWrap::None => language_settings::SoftWrap::EditorWidth,
+                SoftWrap::EditorWidth | SoftWrap::Column(_) => language_settings::SoftWrap::None,
             };
             self.soft_wrap_mode_override = Some(soft_wrap);
         }
@@ -6578,8 +6570,8 @@ impl Editor {
         let buffer = &snapshot.buffer_snapshot;
         let start = buffer.anchor_before(0);
         let end = buffer.anchor_after(buffer.len());
-        let theme = cx.global::<Settings>().theme.as_ref();
-        self.background_highlights_in_range(start..end, &snapshot, theme)
+        let theme = theme::current(cx);
+        self.background_highlights_in_range(start..end, &snapshot, theme.as_ref())
     }
 
     fn document_highlights_for_position<'a>(
@@ -6910,7 +6902,7 @@ impl Editor {
             .map(|a| a.to_string());
 
         let telemetry = project.read(cx).client().telemetry().clone();
-        let telemetry_settings = cx.global::<Settings>().telemetry();
+        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
 
         let event = ClickhouseEvent::Copilot {
             suggestion_id,
@@ -6940,33 +6932,37 @@ impl Editor {
             .and_then(|e| e.to_str())
             .map(|a| a.to_string()));
 
-        let settings = cx.global::<Settings>();
+        let vim_mode = cx
+            .global::<SettingsStore>()
+            .untyped_user_settings()
+            .get("vim_mode")
+            == Some(&serde_json::Value::Bool(true));
+        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+        let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None);
+        let copilot_enabled_for_language = self
+            .buffer
+            .read(cx)
+            .settings_at(0, cx)
+            .show_copilot_suggestions;
 
         let telemetry = project.read(cx).client().telemetry().clone();
         telemetry.report_mixpanel_event(
-                match name {
-                    "open" => "open editor",
-                    "save" => "save editor",
-                    _ => name,
-                },
-                json!({ "File Extension": file_extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true  }),
-                settings.telemetry(),
-            );
+            match name {
+                "open" => "open editor",
+                "save" => "save editor",
+                _ => name,
+            },
+            json!({ "File Extension": file_extension, "Vim Mode": vim_mode, "In Clickhouse": true  }),
+            telemetry_settings,
+        );
         let event = ClickhouseEvent::Editor {
             file_extension,
-            vim_mode: settings.vim_mode,
+            vim_mode,
             operation: name,
-            copilot_enabled: settings.features.copilot,
-            copilot_enabled_for_language: settings.show_copilot_suggestions(
-                self.language_at(0, cx)
-                    .map(|language| language.name())
-                    .as_deref(),
-                self.file_at(0, cx)
-                    .map(|file| file.path().clone())
-                    .as_deref(),
-            ),
+            copilot_enabled,
+            copilot_enabled_for_language,
         };
-        telemetry.report_clickhouse_event(event, settings.telemetry())
+        telemetry.report_clickhouse_event(event, telemetry_settings)
     }
 
     /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
@@ -6998,7 +6994,7 @@ impl Editor {
         let mut lines = Vec::new();
         let mut line: VecDeque<Chunk> = VecDeque::new();
 
-        let theme = &cx.global::<Settings>().theme.editor.syntax;
+        let theme = &theme::current(cx).editor.syntax;
 
         for chunk in chunks {
             let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme));
@@ -7420,7 +7416,7 @@ impl View for Editor {
 }
 
 fn build_style(
-    settings: &Settings,
+    settings: &ThemeSettings,
     get_field_editor_theme: Option<&GetFieldEditorTheme>,
     override_text_style: Option<&OverrideTextStyle>,
     cx: &AppContext,
@@ -7450,7 +7446,7 @@ fn build_style(
         let font_id = font_cache
             .select_font(font_family_id, &font_properties)
             .unwrap();
-        let font_size = settings.buffer_font_size;
+        let font_size = settings.buffer_font_size(cx);
         EditorStyle {
             text: TextStyle {
                 color: settings.theme.editor.text_color,
@@ -7620,10 +7616,10 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
     }
 
     Arc::new(move |cx: &mut BlockContext| {
-        let settings = cx.global::<Settings>();
+        let settings = settings::get::<ThemeSettings>(cx);
         let theme = &settings.theme.editor;
         let style = diagnostic_style(diagnostic.severity, is_valid, theme);
-        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
+        let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
         Flex::column()
             .with_children(highlighted_lines.iter().map(|(line, highlights)| {
                 Label::new(

crates/editor/src/editor_settings.rs 🔗

@@ -0,0 +1,43 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Deserialize)]
+pub struct EditorSettings {
+    pub cursor_blink: bool,
+    pub hover_popover_enabled: bool,
+    pub show_completions_on_input: bool,
+    pub show_scrollbars: ShowScrollbars,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowScrollbars {
+    #[default]
+    Auto,
+    System,
+    Always,
+    Never,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct EditorSettingsContent {
+    pub cursor_blink: Option<bool>,
+    pub hover_popover_enabled: Option<bool>,
+    pub show_completions_on_input: Option<bool>,
+    pub show_scrollbars: Option<ShowScrollbars>,
+}
+
+impl Setting for EditorSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = EditorSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/editor/src/editor_tests.rs 🔗

@@ -12,10 +12,12 @@ use gpui::{
     serde_json, TestAppContext,
 };
 use indoc::indoc;
-use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point};
+use language::{
+    language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
+    BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
+};
 use parking_lot::Mutex;
 use project::FakeFs;
-use settings::EditorSettings;
 use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
 use unindent::Unindent;
 use util::{
@@ -29,7 +31,8 @@ use workspace::{
 
 #[gpui::test]
 fn test_edit_events(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let buffer = cx.add_model(|cx| {
         let mut buffer = language::Buffer::new(0, "123456", cx);
         buffer.set_group_interval(Duration::from_secs(1));
@@ -156,7 +159,8 @@ fn test_edit_events(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let mut now = Instant::now();
     let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
     let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
@@ -226,7 +230,8 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_ime_composition(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let buffer = cx.add_model(|cx| {
         let mut buffer = language::Buffer::new(0, "abcde", cx);
         // Ensure automatic grouping doesn't occur.
@@ -328,7 +333,7 @@ fn test_ime_composition(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_selection_with_mouse(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
 
     let (_, editor) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
@@ -395,7 +400,8 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_canceling_pending_selection(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
         build_editor(buffer, cx)
@@ -429,6 +435,8 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_clone(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
     let (text, selection_ranges) = marked_text_ranges(
         indoc! {"
             one
@@ -439,7 +447,6 @@ fn test_clone(cx: &mut TestAppContext) {
         "},
         true,
     );
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
 
     let (_, editor) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple(&text, cx);
@@ -487,7 +494,8 @@ fn test_clone(cx: &mut TestAppContext) {
 
 #[gpui::test]
 async fn test_navigation_history(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     cx.set_global(DragAndDrop::<Workspace>::default());
     use workspace::item::Item;
 
@@ -600,7 +608,8 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_cancel(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
         build_editor(buffer, cx)
@@ -642,7 +651,8 @@ fn test_cancel(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_fold_action(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple(
             &"
@@ -731,7 +741,8 @@ fn test_fold_action(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_move_cursor(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
     let (_, view) = cx.add_window(|cx| build_editor(buffer.clone(), cx));
 
@@ -806,7 +817,8 @@ fn test_move_cursor(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx);
         build_editor(buffer.clone(), cx)
@@ -910,7 +922,8 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
         build_editor(buffer.clone(), cx)
@@ -959,7 +972,8 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_beginning_end_of_line(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("abc\n  def", cx);
         build_editor(buffer, cx)
@@ -1121,7 +1135,8 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n  {baz.qux()}", cx);
         build_editor(buffer, cx)
@@ -1172,7 +1187,8 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("use one::{\n    two::three::four::five\n};", cx);
         build_editor(buffer, cx)
@@ -1229,6 +1245,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
 
 #[gpui::test]
 async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
     let mut cx = EditorTestContext::new(cx);
 
     let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
@@ -1343,6 +1360,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
     let mut cx = EditorTestContext::new(cx);
     cx.set_state("one «two threeˇ» four");
     cx.update_editor(|editor, cx| {
@@ -1353,7 +1371,8 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("one two three four", cx);
         build_editor(buffer.clone(), cx)
@@ -1388,7 +1407,8 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_newline(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("aaaa\n    bbbb\n", cx);
         build_editor(buffer.clone(), cx)
@@ -1410,7 +1430,8 @@ fn test_newline(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_newline_with_old_selections(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, editor) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple(
             "
@@ -1491,11 +1512,8 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
 
 #[gpui::test]
 async fn test_newline_above(cx: &mut gpui::TestAppContext) {
-    let mut cx = EditorTestContext::new(cx);
-    cx.update(|cx| {
-        cx.update_global::<Settings, _, _>(|settings, _| {
-            settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
-        });
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
     });
 
     let language = Arc::new(
@@ -1506,8 +1524,9 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
         .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
         .unwrap(),
     );
-    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
 
+    let mut cx = EditorTestContext::new(cx);
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
     cx.set_state(indoc! {"
         const a: ˇA = (
             (ˇ
@@ -1516,6 +1535,7 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
             )ˇ
         ˇ);ˇ
     "});
+
     cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
     cx.assert_editor_state(indoc! {"
         ˇ
@@ -1540,11 +1560,8 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_newline_below(cx: &mut gpui::TestAppContext) {
-    let mut cx = EditorTestContext::new(cx);
-    cx.update(|cx| {
-        cx.update_global::<Settings, _, _>(|settings, _| {
-            settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap());
-        });
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
     });
 
     let language = Arc::new(
@@ -1555,8 +1572,9 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
         .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
         .unwrap(),
     );
-    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
 
+    let mut cx = EditorTestContext::new(cx);
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
     cx.set_state(indoc! {"
         const a: ˇA = (
             (ˇ
@@ -1565,6 +1583,7 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
             )ˇ
         ˇ);ˇ
     "});
+
     cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx));
     cx.assert_editor_state(indoc! {"
         const a: A = (
@@ -1589,7 +1608,8 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 fn test_insert_with_old_selections(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, editor) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
         let mut editor = build_editor(buffer.clone(), cx);
@@ -1615,12 +1635,11 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) {
 
 #[gpui::test]
 async fn test_tab(cx: &mut gpui::TestAppContext) {
-    let mut cx = EditorTestContext::new(cx);
-    cx.update(|cx| {
-        cx.update_global::<Settings, _, _>(|settings, _| {
-            settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap());
-        });
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(3)
     });
+
+    let mut cx = EditorTestContext::new(cx);
     cx.set_state(indoc! {"
         ˇabˇc
         ˇ🏀ˇ🏀ˇefg
@@ -1646,6 +1665,8 @@ async fn test_tab(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorTestContext::new(cx);
     let language = Arc::new(
         Language::new(
@@ -1704,7 +1725,10 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAp
 
 #[gpui::test]
 async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
-    let mut cx = EditorTestContext::new(cx);
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4)
+    });
+
     let language = Arc::new(
         Language::new(
             LanguageConfig::default(),
@@ -1713,14 +1737,9 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
         .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
         .unwrap(),
     );
-    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
-
-    cx.update(|cx| {
-        cx.update_global::<Settings, _, _>(|settings, _| {
-            settings.editor_overrides.tab_size = Some(4.try_into().unwrap());
-        });
-    });
 
+    let mut cx = EditorTestContext::new(cx);
+    cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
     cx.set_state(indoc! {"
         fn a() {
             if b {
@@ -1741,6 +1760,10 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |settings| {
+        settings.defaults.tab_size = NonZeroU32::new(4);
+    });
+
     let mut cx = EditorTestContext::new(cx);
 
     cx.set_state(indoc! {"
@@ -1810,13 +1833,12 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
-    let mut cx = EditorTestContext::new(cx);
-    cx.update(|cx| {
-        cx.update_global::<Settings, _, _>(|settings, _| {
-            settings.editor_overrides.hard_tabs = Some(true);
-        });
+    init_test(cx, |settings| {
+        settings.defaults.hard_tabs = Some(true);
     });
 
+    let mut cx = EditorTestContext::new(cx);
+
     // select two ranges on one line
     cx.set_state(indoc! {"
         «oneˇ» «twoˇ»
@@ -1907,25 +1929,25 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
-    cx.update(|cx| {
-        cx.set_global(
-            Settings::test(cx)
-                .with_language_defaults(
-                    "TOML",
-                    EditorSettings {
-                        tab_size: Some(2.try_into().unwrap()),
-                        ..Default::default()
-                    },
-                )
-                .with_language_defaults(
-                    "Rust",
-                    EditorSettings {
-                        tab_size: Some(4.try_into().unwrap()),
-                        ..Default::default()
-                    },
-                ),
-        );
+    init_test(cx, |settings| {
+        settings.languages.extend([
+            (
+                "TOML".into(),
+                LanguageSettingsContent {
+                    tab_size: NonZeroU32::new(2),
+                    ..Default::default()
+                },
+            ),
+            (
+                "Rust".into(),
+                LanguageSettingsContent {
+                    tab_size: NonZeroU32::new(4),
+                    ..Default::default()
+                },
+            ),
+        ]);
     });
+
     let toml_language = Arc::new(Language::new(
         LanguageConfig {
             name: "TOML".into(),
@@ -2020,6 +2042,8 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
 
 #[gpui::test]
 async fn test_backspace(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorTestContext::new(cx);
 
     // Basic backspace
@@ -2067,8 +2091,9 @@ async fn test_backspace(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_delete(cx: &mut gpui::TestAppContext) {
-    let mut cx = EditorTestContext::new(cx);
+    init_test(cx, |_| {});
 
+    let mut cx = EditorTestContext::new(cx);
     cx.set_state(indoc! {"
         onˇe two three
         fou«rˇ» five six
@@ -2095,7 +2120,8 @@ async fn test_delete(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 fn test_delete_line(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
         build_editor(buffer, cx)
@@ -2119,7 +2145,6 @@ fn test_delete_line(cx: &mut TestAppContext) {
         );
     });
 
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
         build_editor(buffer, cx)
@@ -2139,7 +2164,8 @@ fn test_delete_line(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_duplicate_line(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
         build_editor(buffer, cx)
@@ -2191,7 +2217,8 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_move_line_up_down(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
         build_editor(buffer, cx)
@@ -2289,7 +2316,8 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, editor) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
         build_editor(buffer, cx)
@@ -2315,7 +2343,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_transpose(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
 
     _ = cx
         .add_window(|cx| {
@@ -2417,6 +2445,8 @@ fn test_transpose(cx: &mut TestAppContext) {
 
 #[gpui::test]
 async fn test_clipboard(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorTestContext::new(cx);
 
     cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
@@ -2497,6 +2527,8 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorTestContext::new(cx);
     let language = Arc::new(Language::new(
         LanguageConfig::default(),
@@ -2609,7 +2641,8 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 fn test_select_all(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
         build_editor(buffer, cx)
@@ -2625,7 +2658,8 @@ fn test_select_all(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_select_line(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
         build_editor(buffer, cx)
@@ -2671,7 +2705,8 @@ fn test_select_line(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_split_selection_into_lines(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
         build_editor(buffer, cx)
@@ -2741,7 +2776,8 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_add_selection_above_below(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, view) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
         build_editor(buffer, cx)
@@ -2935,6 +2971,8 @@ fn test_add_selection_above_below(cx: &mut TestAppContext) {
 
 #[gpui::test]
 async fn test_select_next(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorTestContext::new(cx);
     cx.set_state("abc\nˇabc abc\ndefabc\nabc");
 
@@ -2959,7 +2997,8 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let language = Arc::new(Language::new(
         LanguageConfig::default(),
         Some(tree_sitter_rust::language()),
@@ -3100,7 +3139,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let language = Arc::new(
         Language::new(
             LanguageConfig {
@@ -3160,6 +3200,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorTestContext::new(cx);
 
     let language = Arc::new(Language::new(
@@ -3329,6 +3371,8 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorTestContext::new(cx);
 
     let html_language = Arc::new(
@@ -3563,6 +3607,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorTestContext::new(cx);
 
     let rust_language = Arc::new(
@@ -3660,7 +3706,8 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let language = Arc::new(Language::new(
         LanguageConfig {
             brackets: BracketPairConfig {
@@ -3814,7 +3861,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let language = Arc::new(Language::new(
         LanguageConfig {
             brackets: BracketPairConfig {
@@ -3919,7 +3967,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_snippets(cx: &mut gpui::TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
 
     let (text, insertion_ranges) = marked_text_ranges(
         indoc! {"
@@ -4027,7 +4075,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx, |_| {});
 
     let mut language = Language::new(
         LanguageConfig {
@@ -4111,16 +4159,14 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
     assert!(!cx.read(|cx| editor.is_dirty(cx)));
 
     // Set rust language override and assert overriden tabsize is sent to language server
-    cx.update(|cx| {
-        cx.update_global::<Settings, _, _>(|settings, _| {
-            settings.language_overrides.insert(
-                "Rust".into(),
-                EditorSettings {
-                    tab_size: Some(8.try_into().unwrap()),
-                    ..Default::default()
-                },
-            );
-        })
+    update_test_settings(cx, |settings| {
+        settings.languages.insert(
+            "Rust".into(),
+            LanguageSettingsContent {
+                tab_size: NonZeroU32::new(8),
+                ..Default::default()
+            },
+        );
     });
 
     let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
@@ -4141,7 +4187,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx, |_| {});
 
     let mut language = Language::new(
         LanguageConfig {
@@ -4227,16 +4273,14 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
     assert!(!cx.read(|cx| editor.is_dirty(cx)));
 
     // Set rust language override and assert overriden tabsize is sent to language server
-    cx.update(|cx| {
-        cx.update_global::<Settings, _, _>(|settings, _| {
-            settings.language_overrides.insert(
-                "Rust".into(),
-                EditorSettings {
-                    tab_size: Some(8.try_into().unwrap()),
-                    ..Default::default()
-                },
-            );
-        })
+    update_test_settings(cx, |settings| {
+        settings.languages.insert(
+            "Rust".into(),
+            LanguageSettingsContent {
+                tab_size: NonZeroU32::new(8),
+                ..Default::default()
+            },
+        );
     });
 
     let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
@@ -4257,7 +4301,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx, |_| {});
 
     let mut language = Language::new(
         LanguageConfig {
@@ -4342,7 +4386,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx, |_| {});
 
     let mut cx = EditorLspTestContext::new_rust(
         lsp::ServerCapabilities {
@@ -4399,7 +4443,7 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx, |_| {});
 
     let mut cx = EditorLspTestContext::new_rust(
         lsp::ServerCapabilities {
@@ -4514,6 +4558,8 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
 
 #[gpui::test]
 async fn test_completion(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorLspTestContext::new_rust(
         lsp::ServerCapabilities {
             completion_provider: Some(lsp::CompletionOptions {
@@ -4651,8 +4697,10 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
     apply_additional_edits.await.unwrap();
 
     cx.update(|cx| {
-        cx.update_global::<Settings, _, _>(|settings, _| {
-            settings.show_completions_on_input = false;
+        cx.update_global::<SettingsStore, _, _>(|settings, cx| {
+            settings.update_user_settings::<EditorSettings>(cx, |settings| {
+                settings.show_completions_on_input = Some(false);
+            });
         })
     });
     cx.set_state("editorˇ");
@@ -4681,7 +4729,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let language = Arc::new(Language::new(
         LanguageConfig {
             line_comment: Some("// ".into()),
@@ -4764,8 +4813,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
-    let mut cx = EditorTestContext::new(cx);
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
 
     let language = Arc::new(Language::new(
         LanguageConfig {
@@ -4778,6 +4826,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
     let registry = Arc::new(LanguageRegistry::test());
     registry.add(language.clone());
 
+    let mut cx = EditorTestContext::new(cx);
     cx.update_buffer(|buffer, cx| {
         buffer.set_language_registry(registry);
         buffer.set_language(Some(language), cx);
@@ -4897,6 +4946,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
 
 #[gpui::test]
 async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorTestContext::new(cx);
 
     let html_language = Arc::new(
@@ -5021,7 +5072,8 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
     let multibuffer = cx.add_model(|cx| {
         let mut multibuffer = MultiBuffer::new(0);
@@ -5067,7 +5119,8 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let markers = vec![('[', ']').into(), ('(', ')').into()];
     let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
         indoc! {"
@@ -5140,7 +5193,8 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_refresh_selections(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
     let mut excerpt1_id = None;
     let multibuffer = cx.add_model(|cx| {
@@ -5224,7 +5278,8 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
 
 #[gpui::test]
 fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx));
     let mut excerpt1_id = None;
     let multibuffer = cx.add_model(|cx| {
@@ -5282,7 +5337,8 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
 
 #[gpui::test]
 async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let language = Arc::new(
         Language::new(
             LanguageConfig {
@@ -5355,7 +5411,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 fn test_highlighted_ranges(cx: &mut TestAppContext) {
-    cx.update(|cx| cx.set_global(Settings::test(cx)));
+    init_test(cx, |_| {});
+
     let (_, editor) = cx.add_window(|cx| {
         let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
         build_editor(buffer.clone(), cx)
@@ -5395,7 +5452,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
         let mut highlighted_ranges = editor.background_highlights_in_range(
             anchor_range(Point::new(3, 4)..Point::new(7, 4)),
             &snapshot,
-            cx.global::<Settings>().theme.as_ref(),
+            theme::current(cx).as_ref(),
         );
         // Enforce a consistent ordering based on color without relying on the ordering of the
         // highlight's `TypeId` which is non-deterministic.
@@ -5425,7 +5482,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
             editor.background_highlights_in_range(
                 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
                 &snapshot,
-                cx.global::<Settings>().theme.as_ref(),
+                theme::current(cx).as_ref(),
             ),
             &[(
                 DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
@@ -5437,7 +5494,8 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
 
 #[gpui::test]
 async fn test_following(cx: &mut gpui::TestAppContext) {
-    Settings::test_async(cx);
+    init_test(cx, |_| {});
+
     let fs = FakeFs::new(cx.background());
     let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
 
@@ -5576,7 +5634,8 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
-    Settings::test_async(cx);
+    init_test(cx, |_| {});
+
     let fs = FakeFs::new(cx.background());
     let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
     let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@@ -5805,6 +5864,8 @@ fn test_combine_syntax_and_fuzzy_match_highlights() {
 
 #[gpui::test]
 async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorTestContext::new(cx);
 
     let diff_base = r#"
@@ -5924,6 +5985,8 @@ fn test_split_words() {
 
 #[gpui::test]
 async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
     let mut assert = |before, after| {
         let _state_context = cx.set_state(before);
@@ -5972,6 +6035,8 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test(iterations = 10)]
 async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
+
     let (copilot, copilot_lsp) = Copilot::fake(cx);
     cx.update(|cx| cx.set_global(copilot));
     let mut cx = EditorLspTestContext::new_rust(
@@ -6223,6 +6288,8 @@ async fn test_copilot_completion_invalidation(
     deterministic: Arc<Deterministic>,
     cx: &mut gpui::TestAppContext,
 ) {
+    init_test(cx, |_| {});
+
     let (copilot, copilot_lsp) = Copilot::fake(cx);
     cx.update(|cx| cx.set_global(copilot));
     let mut cx = EditorLspTestContext::new_rust(
@@ -6288,11 +6355,10 @@ async fn test_copilot_multibuffer(
     deterministic: Arc<Deterministic>,
     cx: &mut gpui::TestAppContext,
 ) {
+    init_test(cx, |_| {});
+
     let (copilot, copilot_lsp) = Copilot::fake(cx);
-    cx.update(|cx| {
-        cx.set_global(Settings::test(cx));
-        cx.set_global(copilot)
-    });
+    cx.update(|cx| cx.set_global(copilot));
 
     let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx));
     let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx));
@@ -6392,14 +6458,16 @@ async fn test_copilot_disabled_globs(
     deterministic: Arc<Deterministic>,
     cx: &mut gpui::TestAppContext,
 ) {
-    let (copilot, copilot_lsp) = Copilot::fake(cx);
-    cx.update(|cx| {
-        let mut settings = Settings::test(cx);
-        settings.copilot.disabled_globs = vec![glob::Pattern::new(".env*").unwrap()];
-        cx.set_global(settings);
-        cx.set_global(copilot)
+    init_test(cx, |settings| {
+        settings
+            .copilot
+            .get_or_insert(Default::default())
+            .disabled_globs = Some(vec![".env*".to_string()]);
     });
 
+    let (copilot, copilot_lsp) = Copilot::fake(cx);
+    cx.update(|cx| cx.set_global(copilot));
+
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
         "/test",
@@ -6596,3 +6664,30 @@ fn handle_copilot_completion_request(
         }
     });
 }
+
+pub(crate) fn update_test_settings(
+    cx: &mut TestAppContext,
+    f: impl Fn(&mut AllLanguageSettingsContent),
+) {
+    cx.update(|cx| {
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            store.update_user_settings::<AllLanguageSettings>(cx, f);
+        });
+    });
+}
+
+pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
+    cx.foreground().forbid_parking();
+
+    cx.update(|cx| {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+        client::init_settings(cx);
+        language::init(cx);
+        Project::init_settings(cx);
+        workspace::init_settings(cx);
+        crate::init(cx);
+    });
+
+    update_test_settings(cx, f);
+}

crates/editor/src/element.rs 🔗

@@ -5,6 +5,7 @@ use super::{
 };
 use crate::{
     display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
+    editor_settings::ShowScrollbars,
     git::{diff_hunk_to_display, DisplayDiffHunk},
     hover_popover::{
         hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
@@ -13,7 +14,7 @@ use crate::{
     link_go_to_definition::{
         go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
     },
-    mouse_context_menu, EditorStyle, GutterHover, UnfoldAt,
+    mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
 };
 use clock::ReplicaId;
 use collections::{BTreeMap, HashMap};
@@ -35,9 +36,11 @@ use gpui::{
 };
 use itertools::Itertools;
 use json::json;
-use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Selection};
+use language::{
+    language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16,
+    Selection,
+};
 use project::ProjectPath;
-use settings::{GitGutter, Settings, ShowWhitespaces};
 use smallvec::SmallVec;
 use std::{
     borrow::Cow,
@@ -47,7 +50,7 @@ use std::{
     ops::Range,
     sync::Arc,
 };
-use workspace::item::Item;
+use workspace::{item::Item, GitGutterSetting, WorkspaceSettings};
 
 enum FoldMarkers {}
 
@@ -547,11 +550,11 @@ impl EditorElement {
         let scroll_top = scroll_position.y() * line_height;
 
         let show_gutter = matches!(
-            &cx.global::<Settings>()
-                .git_overrides
+            settings::get::<WorkspaceSettings>(cx)
+                .git
                 .git_gutter
                 .unwrap_or_default(),
-            GitGutter::TrackedFiles
+            GitGutterSetting::TrackedFiles
         );
 
         if show_gutter {
@@ -608,7 +611,7 @@ impl EditorElement {
         layout: &mut LayoutState,
         cx: &mut ViewContext<Editor>,
     ) {
-        let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
+        let diff_style = &theme::current(cx).editor.diff.clone();
         let line_height = layout.position_map.line_height;
 
         let scroll_position = layout.position_map.snapshot.scroll_position();
@@ -708,6 +711,7 @@ impl EditorElement {
         let scroll_left = scroll_position.x() * max_glyph_width;
         let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
         let line_end_overshoot = 0.15 * layout.position_map.line_height;
+        let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
 
         scene.push_layer(Some(bounds));
 
@@ -882,9 +886,10 @@ impl EditorElement {
                     content_origin,
                     scroll_left,
                     visible_text_bounds,
-                    cx,
+                    whitespace_setting,
                     &invisible_display_ranges,
                     visible_bounds,
+                    cx,
                 )
             }
         }
@@ -1046,7 +1051,7 @@ impl EditorElement {
                 ..Default::default()
             });
 
-            let diff_style = cx.global::<Settings>().theme.editor.diff.clone();
+            let diff_style = theme::current(cx).editor.diff.clone();
             for hunk in layout
                 .position_map
                 .snapshot
@@ -1457,7 +1462,7 @@ impl EditorElement {
         editor: &mut Editor,
         cx: &mut LayoutContext<Editor>,
     ) -> (f32, Vec<BlockLayout>) {
-        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+        let tooltip_style = theme::current(cx).tooltip.clone();
         let scroll_x = snapshot.scroll_anchor.offset.x();
         let (fixed_blocks, non_fixed_blocks) = snapshot
             .blocks_in_range(rows.clone())
@@ -1783,9 +1788,10 @@ impl LineWithInvisibles {
         content_origin: Vector2F,
         scroll_left: f32,
         visible_text_bounds: RectF,
-        cx: &mut ViewContext<Editor>,
+        whitespace_setting: ShowWhitespaceSetting,
         selection_ranges: &[Range<DisplayPoint>],
         visible_bounds: RectF,
+        cx: &mut ViewContext<Editor>,
     ) {
         let line_height = layout.position_map.line_height;
         let line_y = row as f32 * line_height - scroll_top;
@@ -1799,7 +1805,6 @@ impl LineWithInvisibles {
         );
 
         self.draw_invisibles(
-            cx,
             &selection_ranges,
             layout,
             content_origin,
@@ -1809,12 +1814,13 @@ impl LineWithInvisibles {
             scene,
             visible_bounds,
             line_height,
+            whitespace_setting,
+            cx,
         );
     }
 
     fn draw_invisibles(
         &self,
-        cx: &mut ViewContext<Editor>,
         selection_ranges: &[Range<DisplayPoint>],
         layout: &LayoutState,
         content_origin: Vector2F,
@@ -1824,17 +1830,13 @@ impl LineWithInvisibles {
         scene: &mut SceneBuilder,
         visible_bounds: RectF,
         line_height: f32,
+        whitespace_setting: ShowWhitespaceSetting,
+        cx: &mut ViewContext<Editor>,
     ) {
-        let settings = cx.global::<Settings>();
-        let allowed_invisibles_regions = match settings
-            .editor_overrides
-            .show_whitespaces
-            .or(settings.editor_defaults.show_whitespaces)
-            .unwrap_or_default()
-        {
-            ShowWhitespaces::None => return,
-            ShowWhitespaces::Selection => Some(selection_ranges),
-            ShowWhitespaces::All => None,
+        let allowed_invisibles_regions = match whitespace_setting {
+            ShowWhitespaceSetting::None => return,
+            ShowWhitespaceSetting::Selection => Some(selection_ranges),
+            ShowWhitespaceSetting::All => None,
         };
 
         for invisible in &self.invisibles {
@@ -1979,11 +1981,11 @@ impl Element<Editor> for EditorElement {
         let is_singleton = editor.is_singleton(cx);
 
         let highlighted_rows = editor.highlighted_rows();
-        let theme = cx.global::<Settings>().theme.as_ref();
+        let theme = theme::current(cx);
         let highlighted_ranges = editor.background_highlights_in_range(
             start_anchor..end_anchor,
             &snapshot.display_snapshot,
-            theme,
+            theme.as_ref(),
         );
 
         fold_ranges.extend(
@@ -2058,13 +2060,13 @@ impl Element<Editor> for EditorElement {
             ));
         }
 
-        let show_scrollbars = match cx.global::<Settings>().show_scrollbars {
-            settings::ShowScrollbars::Auto => {
+        let show_scrollbars = match settings::get::<EditorSettings>(cx).show_scrollbars {
+            ShowScrollbars::Auto => {
                 snapshot.has_scrollbar_info() || editor.scroll_manager.scrollbars_visible()
             }
-            settings::ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(),
-            settings::ShowScrollbars::Always => true,
-            settings::ShowScrollbars::Never => false,
+            ShowScrollbars::System => editor.scroll_manager.scrollbars_visible(),
+            ShowScrollbars::Always => true,
+            ShowScrollbars::Never => false,
         };
 
         let include_root = editor
@@ -2826,17 +2828,19 @@ mod tests {
     use super::*;
     use crate::{
         display_map::{BlockDisposition, BlockProperties},
+        editor_tests::{init_test, update_test_settings},
         Editor, MultiBuffer,
     };
     use gpui::TestAppContext;
+    use language::language_settings;
     use log::info;
-    use settings::Settings;
     use std::{num::NonZeroU32, sync::Arc};
     use util::test::sample_text;
 
     #[gpui::test]
     fn test_layout_line_numbers(cx: &mut TestAppContext) {
-        cx.update(|cx| cx.set_global(Settings::test(cx)));
+        init_test(cx, |_| {});
+
         let (_, editor) = cx.add_window(|cx| {
             let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
             Editor::new(EditorMode::Full, buffer, None, None, cx)
@@ -2854,7 +2858,8 @@ mod tests {
 
     #[gpui::test]
     fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
-        cx.update(|cx| cx.set_global(Settings::test(cx)));
+        init_test(cx, |_| {});
+
         let (_, editor) = cx.add_window(|cx| {
             let buffer = MultiBuffer::build_simple("", cx);
             Editor::new(EditorMode::Full, buffer, None, None, cx)
@@ -2914,26 +2919,27 @@ mod tests {
 
     #[gpui::test]
     fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
-        let tab_size = 4;
+        const TAB_SIZE: u32 = 4;
+
         let input_text = "\t \t|\t| a b";
         let expected_invisibles = vec![
             Invisible::Tab {
                 line_start_offset: 0,
             },
             Invisible::Whitespace {
-                line_offset: tab_size as usize,
+                line_offset: TAB_SIZE as usize,
             },
             Invisible::Tab {
-                line_start_offset: tab_size as usize + 1,
+                line_start_offset: TAB_SIZE as usize + 1,
             },
             Invisible::Tab {
-                line_start_offset: tab_size as usize * 2 + 1,
+                line_start_offset: TAB_SIZE as usize * 2 + 1,
             },
             Invisible::Whitespace {
-                line_offset: tab_size as usize * 3 + 1,
+                line_offset: TAB_SIZE as usize * 3 + 1,
             },
             Invisible::Whitespace {
-                line_offset: tab_size as usize * 3 + 3,
+                line_offset: TAB_SIZE as usize * 3 + 3,
             },
         ];
         assert_eq!(
@@ -2945,12 +2951,11 @@ mod tests {
             "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
         );
 
-        cx.update(|cx| {
-            let mut test_settings = Settings::test(cx);
-            test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
-            test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
-            cx.set_global(test_settings);
+        init_test(cx, |s| {
+            s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+            s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
         });
+
         let actual_invisibles =
             collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0);
 
@@ -2959,11 +2964,9 @@ mod tests {
 
     #[gpui::test]
     fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let mut test_settings = Settings::test(cx);
-            test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
-            test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(4).unwrap());
-            cx.set_global(test_settings);
+        init_test(cx, |s| {
+            s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+            s.defaults.tab_size = NonZeroU32::new(4);
         });
 
         for editor_mode_without_invisibles in [
@@ -3014,19 +3017,18 @@ mod tests {
         );
         info!("Expected invisibles: {expected_invisibles:?}");
 
+        init_test(cx, |_| {});
+
         // Put the same string with repeating whitespace pattern into editors of various size,
         // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
         let resize_step = 10.0;
         let mut editor_width = 200.0;
         while editor_width <= 1000.0 {
-            cx.update(|cx| {
-                let mut test_settings = Settings::test(cx);
-                test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap());
-                test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All);
-                test_settings.editor_defaults.preferred_line_length = Some(editor_width as u32);
-                test_settings.editor_defaults.soft_wrap =
-                    Some(settings::SoftWrap::PreferredLineLength);
-                cx.set_global(test_settings);
+            update_test_settings(cx, |s| {
+                s.defaults.tab_size = NonZeroU32::new(tab_size);
+                s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+                s.defaults.preferred_line_length = Some(editor_width as u32);
+                s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
             });
 
             let actual_invisibles =
@@ -3074,7 +3076,7 @@ mod tests {
 
         let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
         let (_, layout_state) = editor.update(cx, |editor, cx| {
-            editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx);
+            editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
             editor.set_wrap_width(Some(editor_width), cx);
 
             let mut new_parents = Default::default();

crates/editor/src/highlight_matching_bracket.rs 🔗

@@ -33,12 +33,14 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test::editor_lsp_test_context::EditorLspTestContext;
+    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
     use indoc::indoc;
     use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
 
     #[gpui::test]
     async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
         let mut cx = EditorLspTestContext::new(
             Language::new(
                 LanguageConfig {

crates/editor/src/hover_popover.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
-    EditorStyle, RangeToAnchorExt,
+    display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings,
+    EditorSnapshot, EditorStyle, RangeToAnchorExt,
 };
 use futures::FutureExt;
 use gpui::{
@@ -12,7 +12,6 @@ use gpui::{
 };
 use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
 use project::{HoverBlock, HoverBlockKind, Project};
-use settings::Settings;
 use std::{ops::Range, sync::Arc, time::Duration};
 use util::TryFutureExt;
 
@@ -38,7 +37,7 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
 /// The internal hover action dispatches between `show_hover` or `hide_hover`
 /// depending on whether a point to hover over is provided.
 pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
-    if cx.global::<Settings>().hover_popover_enabled {
+    if settings::get::<EditorSettings>(cx).hover_popover_enabled {
         if let Some(point) = point {
             show_hover(editor, point, false, cx);
         } else {
@@ -654,7 +653,7 @@ impl DiagnosticPopover {
             _ => style.hover_popover.container,
         };
 
-        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+        let tooltip_style = theme::current(cx).tooltip.clone();
 
         MouseEventHandler::<DiagnosticPopover, _>::new(0, cx, |_, _| {
             text.with_soft_wrap(true)
@@ -694,7 +693,7 @@ impl DiagnosticPopover {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test::editor_lsp_test_context::EditorLspTestContext;
+    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
     use gpui::fonts::Weight;
     use indoc::indoc;
     use language::{Diagnostic, DiagnosticSet};
@@ -706,6 +705,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
         let mut cx = EditorLspTestContext::new_rust(
             lsp::ServerCapabilities {
                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -773,6 +774,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
         let mut cx = EditorLspTestContext::new_rust(
             lsp::ServerCapabilities {
                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -816,6 +819,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
         let mut cx = EditorLspTestContext::new_rust(
             lsp::ServerCapabilities {
                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -882,7 +887,8 @@ mod tests {
 
     #[gpui::test]
     fn test_render_blocks(cx: &mut gpui::TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx, |_| {});
+
         cx.add_window(|cx| {
             let editor = Editor::single_line(None, cx);
             let style = editor.style(cx);

crates/editor/src/items.rs 🔗

@@ -16,7 +16,6 @@ use language::{
 };
 use project::{FormatTrigger, Item as _, Project, ProjectPath};
 use rpc::proto::{self, update_view};
-use settings::Settings;
 use smallvec::SmallVec;
 use std::{
     borrow::Cow,
@@ -1116,7 +1115,7 @@ impl View for CursorPosition {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         if let Some(position) = self.position {
-            let theme = &cx.global::<Settings>().theme.workspace.status_bar;
+            let theme = &theme::current(cx).workspace.status_bar;
             let mut text = format!(
                 "{}{FILE_ROW_COLUMN_DELIMITER}{}",
                 position.row + 1,
@@ -1,10 +1,8 @@
-use std::ops::Range;
-
 use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
 use gpui::{Task, ViewContext};
 use language::{Bias, ToOffset};
 use project::LocationLink;
-use settings::Settings;
+use std::ops::Range;
 use util::TryFutureExt;
 
 #[derive(Debug, Default)]
@@ -211,7 +209,7 @@ pub fn show_link_definition(
                         });
 
                         // Highlight symbol using theme link definition highlight style
-                        let style = cx.global::<Settings>().theme.editor.link_definition;
+                        let style = theme::current(cx).editor.link_definition;
                         this.highlight_text::<LinkGoToDefinitionState>(
                             vec![highlight_range],
                             style,
@@ -297,6 +295,8 @@ fn go_to_fetched_definition_of_kind(
 
 #[cfg(test)]
 mod tests {
+    use super::*;
+    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
     use futures::StreamExt;
     use gpui::{
         platform::{self, Modifiers, ModifiersChangedEvent},
@@ -305,12 +305,10 @@ mod tests {
     use indoc::indoc;
     use lsp::request::{GotoDefinition, GotoTypeDefinition};
 
-    use crate::test::editor_lsp_test_context::EditorLspTestContext;
-
-    use super::*;
-
     #[gpui::test]
     async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
         let mut cx = EditorLspTestContext::new_rust(
             lsp::ServerCapabilities {
                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
@@ -417,6 +415,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
         let mut cx = EditorLspTestContext::new_rust(
             lsp::ServerCapabilities {
                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),

crates/editor/src/mouse_context_menu.rs 🔗

@@ -57,13 +57,14 @@ pub fn deploy_context_menu(
 
 #[cfg(test)]
 mod tests {
-    use crate::test::editor_lsp_test_context::EditorLspTestContext;
-
     use super::*;
+    use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
     use indoc::indoc;
 
     #[gpui::test]
     async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
+        init_test(cx, |_| {});
+
         let mut cx = EditorLspTestContext::new_rust(
             lsp::ServerCapabilities {
                 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),

crates/editor/src/movement.rs 🔗

@@ -369,11 +369,12 @@ pub fn split_display_range_by_lines(
 mod tests {
     use super::*;
     use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
-    use settings::Settings;
+    use settings::SettingsStore;
 
     #[gpui::test]
     fn test_previous_word_start(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
+
         fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -400,7 +401,8 @@ mod tests {
 
     #[gpui::test]
     fn test_previous_subword_start(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
+
         fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -434,7 +436,8 @@ mod tests {
 
     #[gpui::test]
     fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
+
         fn assert(
             marked_text: &str,
             cx: &mut gpui::AppContext,
@@ -466,7 +469,8 @@ mod tests {
 
     #[gpui::test]
     fn test_next_word_end(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
+
         fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -490,7 +494,8 @@ mod tests {
 
     #[gpui::test]
     fn test_next_subword_end(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
+
         fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -523,7 +528,8 @@ mod tests {
 
     #[gpui::test]
     fn test_find_boundary(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
+
         fn assert(
             marked_text: &str,
             cx: &mut gpui::AppContext,
@@ -555,7 +561,8 @@ mod tests {
 
     #[gpui::test]
     fn test_surrounding_word(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
+
         fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
             let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
             assert_eq!(
@@ -576,7 +583,8 @@ mod tests {
 
     #[gpui::test]
     fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
-        cx.set_global(Settings::test(cx));
+        init_test(cx);
+
         let family_id = cx
             .font_cache()
             .load_family(&["Helvetica"], &Default::default())
@@ -691,4 +699,11 @@ mod tests {
             (DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
         );
     }
+
+    fn init_test(cx: &mut gpui::AppContext) {
+        cx.set_global(SettingsStore::test(cx));
+        theme::init((), cx);
+        language::init(cx);
+        crate::init(cx);
+    }
 }

crates/editor/src/multi_buffer.rs 🔗

@@ -9,7 +9,9 @@ use git::diff::DiffHunk;
 use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
 pub use language::Completion;
 use language::{
-    char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
+    char_kind,
+    language_settings::{language_settings, LanguageSettings},
+    AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
     DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
     Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
     ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
@@ -1372,6 +1374,15 @@ impl MultiBuffer {
             .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
     }
 
+    pub fn settings_at<'a, T: ToOffset>(
+        &self,
+        point: T,
+        cx: &'a AppContext,
+    ) -> &'a LanguageSettings {
+        let language = self.language_at(point, cx);
+        language_settings(language.map(|l| l.name()).as_deref(), cx)
+    }
+
     pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
         self.buffers
             .borrow()
@@ -2764,6 +2775,16 @@ impl MultiBufferSnapshot {
             .and_then(|(buffer, offset)| buffer.language_at(offset))
     }
 
+    pub fn settings_at<'a, T: ToOffset>(
+        &'a self,
+        point: T,
+        cx: &'a AppContext,
+    ) -> &'a LanguageSettings {
+        self.point_to_buffer_offset(point)
+            .map(|(buffer, offset)| buffer.settings_at(offset, cx))
+            .unwrap_or_else(|| language_settings(None, cx))
+    }
+
     pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
         self.point_to_buffer_offset(point)
             .and_then(|(buffer, offset)| buffer.language_scope_at(offset))
@@ -3785,10 +3806,9 @@ mod tests {
     use gpui::{AppContext, TestAppContext};
     use language::{Buffer, Rope};
     use rand::prelude::*;
-    use settings::Settings;
+    use settings::SettingsStore;
     use std::{env, rc::Rc};
     use unindent::Unindent;
-
     use util::test::sample_text;
 
     #[gpui::test]
@@ -5034,7 +5054,8 @@ mod tests {
 
     #[gpui::test]
     fn test_history(cx: &mut AppContext) {
-        cx.set_global(Settings::test(cx));
+        cx.set_global(SettingsStore::test(cx));
+
         let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx));
         let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx));
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));

crates/editor/src/test/editor_lsp_test_context.rs 🔗

@@ -34,13 +34,17 @@ impl<'a> EditorLspTestContext<'a> {
     ) -> EditorLspTestContext<'a> {
         use json::json;
 
+        let app_state = cx.update(AppState::test);
+
         cx.update(|cx| {
+            theme::init((), cx);
+            language::init(cx);
             crate::init(cx);
             pane::init(cx);
+            Project::init_settings(cx);
+            workspace::init_settings(cx);
         });
 
-        let app_state = cx.update(AppState::test);
-
         let file_name = format!(
             "file.{}",
             language

crates/editor/src/test/editor_test_context.rs 🔗

@@ -1,19 +1,16 @@
-use std::{
-    any::TypeId,
-    ops::{Deref, DerefMut, Range},
-};
-
-use futures::Future;
-use indoc::indoc;
-
 use crate::{
     display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
 };
+use futures::Future;
 use gpui::{
     keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle,
 };
+use indoc::indoc;
 use language::{Buffer, BufferSnapshot};
-use settings::Settings;
+use std::{
+    any::TypeId,
+    ops::{Deref, DerefMut, Range},
+};
 use util::{
     assert_set_eq,
     test::{generate_marked_text, marked_text_ranges},
@@ -30,15 +27,10 @@ pub struct EditorTestContext<'a> {
 impl<'a> EditorTestContext<'a> {
     pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
         let (window_id, editor) = cx.update(|cx| {
-            cx.set_global(Settings::test(cx));
-            crate::init(cx);
-
-            let (window_id, editor) = cx.add_window(Default::default(), |cx| {
+            cx.add_window(Default::default(), |cx| {
                 cx.focus_self();
                 build_editor(MultiBuffer::build_simple("", cx), cx)
-            });
-
-            (window_id, editor)
+            })
         });
 
         Self {

crates/feedback/src/deploy_feedback_button.rs 🔗

@@ -3,7 +3,6 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     Entity, View, ViewContext, WeakViewHandle,
 };
-use settings::Settings;
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
 use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
@@ -33,7 +32,7 @@ impl View for DeployFeedbackButton {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         let active = self.active;
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
         Stack::new()
             .with_child(
                 MouseEventHandler::<Self, Self>::new(0, cx, |state, _| {

crates/feedback/src/feedback_info_text.rs 🔗

@@ -3,7 +3,6 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     AnyElement, Element, Entity, View, ViewContext, ViewHandle,
 };
-use settings::Settings;
 use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
 
 use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo};
@@ -30,7 +29,7 @@ impl View for FeedbackInfoText {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
 
         Flex::row()
             .with_child(

crates/feedback/src/submit_feedback_button.rs 🔗

@@ -5,7 +5,6 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle,
 };
-use settings::Settings;
 use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
 
 pub fn init(cx: &mut AppContext) {
@@ -46,7 +45,7 @@ impl View for SubmitFeedbackButton {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
         enum SubmitFeedbackButton {}
         MouseEventHandler::<SubmitFeedbackButton, Self>::new(0, cx, |state, _| {
             let style = theme.feedback.submit_button.style_for(state, false);

crates/file_finder/Cargo.toml 🔗

@@ -24,7 +24,9 @@ postage.workspace = true
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
-serde_json.workspace = true
+language = { path = "../language", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }
+
+serde_json.workspace = true
 ctor.workspace = true
 env_logger.workspace = true

crates/file_finder/src/file_finder.rs 🔗

@@ -5,7 +5,6 @@ use gpui::{
 };
 use picker::{Picker, PickerDelegate};
 use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
-use settings::Settings;
 use std::{
     path::Path,
     sync::{
@@ -324,8 +323,8 @@ impl PickerDelegate for FileFinderDelegate {
         cx: &AppContext,
     ) -> AnyElement<Picker<Self>> {
         let path_match = &self.matches[ix];
-        let settings = cx.global::<Settings>();
-        let style = settings.theme.picker.item.style_for(mouse_state, selected);
+        let theme = theme::current(cx);
+        let style = theme.picker.item.style_for(mouse_state, selected);
         let (file_name, file_name_positions, full_path, full_path_positions) =
             self.labels_for_match(path_match);
         Flex::column()
@@ -344,9 +343,11 @@ impl PickerDelegate for FileFinderDelegate {
 
 #[cfg(test)]
 mod tests {
+    use std::time::Duration;
+
     use super::*;
     use editor::Editor;
-    use gpui::executor::Deterministic;
+    use gpui::TestAppContext;
     use menu::{Confirm, SelectNext};
     use serde_json::json;
     use workspace::{AppState, Workspace};
@@ -359,13 +360,8 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_matching_paths(cx: &mut gpui::TestAppContext) {
-        let app_state = cx.update(|cx| {
-            super::init(cx);
-            editor::init(cx);
-            AppState::test(cx)
-        });
-
+    async fn test_matching_paths(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -415,15 +411,8 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_row_column_numbers_query_inside_file(
-        deterministic: Arc<Deterministic>,
-        cx: &mut gpui::TestAppContext,
-    ) {
-        let app_state = cx.update(|cx| {
-            super::init(cx);
-            editor::init(cx);
-            AppState::test(cx)
-        });
+    async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
 
         let first_file_name = "first.rs";
         let first_file_contents = "// First Rust file";
@@ -484,9 +473,9 @@ mod tests {
             let active_item = active_pane.read(cx).active_item().unwrap();
             active_item.downcast::<Editor>().unwrap()
         });
-        deterministic.advance_clock(std::time::Duration::from_secs(2));
-        deterministic.start_waiting();
-        deterministic.finish_waiting();
+        cx.foreground().advance_clock(Duration::from_secs(2));
+        cx.foreground().start_waiting();
+        cx.foreground().finish_waiting();
         editor.update(cx, |editor, cx| {
             let all_selections = editor.selections.all_adjusted(cx);
             assert_eq!(
@@ -505,15 +494,8 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_row_column_numbers_query_outside_file(
-        deterministic: Arc<Deterministic>,
-        cx: &mut gpui::TestAppContext,
-    ) {
-        let app_state = cx.update(|cx| {
-            super::init(cx);
-            editor::init(cx);
-            AppState::test(cx)
-        });
+    async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
 
         let first_file_name = "first.rs";
         let first_file_contents = "// First Rust file";
@@ -574,9 +556,9 @@ mod tests {
             let active_item = active_pane.read(cx).active_item().unwrap();
             active_item.downcast::<Editor>().unwrap()
         });
-        deterministic.advance_clock(std::time::Duration::from_secs(2));
-        deterministic.start_waiting();
-        deterministic.finish_waiting();
+        cx.foreground().advance_clock(Duration::from_secs(2));
+        cx.foreground().start_waiting();
+        cx.foreground().finish_waiting();
         editor.update(cx, |editor, cx| {
             let all_selections = editor.selections.all_adjusted(cx);
             assert_eq!(
@@ -595,8 +577,8 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) {
-        let app_state = cx.update(AppState::test);
+    async fn test_matching_cancellation(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -664,8 +646,8 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_ignored_files(cx: &mut gpui::TestAppContext) {
-        let app_state = cx.update(AppState::test);
+    async fn test_ignored_files(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -720,8 +702,8 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) {
-        let app_state = cx.update(AppState::test);
+    async fn test_single_file_worktrees(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -778,10 +760,8 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_multiple_matches_with_same_relative_path(cx: &mut gpui::TestAppContext) {
-        cx.foreground().forbid_parking();
-
-        let app_state = cx.update(AppState::test);
+    async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -834,10 +814,8 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) {
-        cx.foreground().forbid_parking();
-
-        let app_state = cx.update(AppState::test);
+    async fn test_path_distance_ordering(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -886,8 +864,8 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) {
-        let app_state = cx.update(AppState::test);
+    async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -926,6 +904,19 @@ mod tests {
         });
     }
 
+    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            let state = AppState::test(cx);
+            theme::init((), cx);
+            language::init(cx);
+            super::init(cx);
+            editor::init(cx);
+            workspace::init_settings(cx);
+            state
+        })
+    }
+
     fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
         PathLikeWithPosition::parse_str(test_str, |path_like_str| {
             Ok::<_, std::convert::Infallible>(FileSearchQuery {

crates/go_to_line/Cargo.toml 🔗

@@ -16,4 +16,5 @@ settings = { path = "../settings" }
 text = { path = "../text" }
 workspace = { path = "../workspace" }
 postage.workspace = true
+theme = { path = "../theme" }
 util = { path = "../util" }

crates/go_to_line/src/go_to_line.rs 🔗

@@ -6,7 +6,6 @@ use gpui::{
     View, ViewContext, ViewHandle,
 };
 use menu::{Cancel, Confirm};
-use settings::Settings;
 use text::{Bias, Point};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
 use workspace::{Modal, Workspace};
@@ -151,7 +150,7 @@ impl View for GoToLine {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &cx.global::<Settings>().theme.picker;
+        let theme = &theme::current(cx).picker;
 
         let label = format!(
             "{}{FILE_ROW_COLUMN_DELIMITER}{} of {} lines",

crates/gpui/src/app.rs 🔗

@@ -1174,7 +1174,7 @@ impl AppContext {
             this.notify_global(type_id);
             result
         } else {
-            panic!("No global added for {}", std::any::type_name::<T>());
+            panic!("no global added for {}", std::any::type_name::<T>());
         }
     }
 
@@ -1182,6 +1182,15 @@ impl AppContext {
         self.globals.clear();
     }
 
+    pub fn remove_global<T: 'static>(&mut self) -> T {
+        *self
+            .globals
+            .remove(&TypeId::of::<T>())
+            .unwrap_or_else(|| panic!("no global added for {}", std::any::type_name::<T>()))
+            .downcast()
+            .unwrap()
+    }
+
     pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
     where
         T: Entity,

crates/gpui/src/executor.rs 🔗

@@ -477,6 +477,14 @@ impl Deterministic {
         state.rng = StdRng::seed_from_u64(state.seed);
     }
 
+    pub fn allow_parking(&self) {
+        use rand::prelude::*;
+
+        let mut state = self.state.lock();
+        state.forbid_parking = false;
+        state.rng = StdRng::seed_from_u64(state.seed);
+    }
+
     pub async fn simulate_random_delay(&self) {
         use rand::prelude::*;
         use smol::future::yield_now;
@@ -698,6 +706,14 @@ impl Foreground {
         }
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn allow_parking(&self) {
+        match self {
+            Self::Deterministic { executor, .. } => executor.allow_parking(),
+            _ => panic!("this method can only be called on a deterministic executor"),
+        }
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn advance_clock(&self, duration: Duration) {
         match self {

crates/journal/Cargo.toml 🔗

@@ -13,9 +13,12 @@ editor = { path = "../editor" }
 gpui = { path = "../gpui" }
 util = { path = "../util" }
 workspace = { path = "../workspace" }
+settings = { path = "../settings" }
+
 anyhow.workspace = true
 chrono = "0.4"
 dirs = "4.0"
+serde.workspace = true
+schemars.workspace = true
 log.workspace = true
-settings = { path = "../settings" }
 shellexpand = "2.1.0"

crates/journal/src/journal.rs 🔗

@@ -1,7 +1,9 @@
+use anyhow::Result;
 use chrono::{Datelike, Local, NaiveTime, Timelike};
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{actions, AppContext};
-use settings::{HourFormat, Settings};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
 use std::{
     fs::OpenOptions,
     path::{Path, PathBuf},
@@ -11,13 +13,48 @@ use workspace::AppState;
 
 actions!(journal, [NewJournalEntry]);
 
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+pub struct JournalSettings {
+    pub path: Option<String>,
+    pub hour_format: Option<HourFormat>,
+}
+
+impl Default for JournalSettings {
+    fn default() -> Self {
+        Self {
+            path: Some("~".into()),
+            hour_format: Some(Default::default()),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum HourFormat {
+    #[default]
+    Hour12,
+    Hour24,
+}
+
+impl settings::Setting for JournalSettings {
+    const KEY: Option<&'static str> = Some("journal");
+
+    type FileContent = Self;
+
+    fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
+    settings::register::<JournalSettings>(cx);
+
     cx.add_global_action(move |_: &NewJournalEntry, cx| new_journal_entry(app_state.clone(), cx));
 }
 
 pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
-    let settings = cx.global::<Settings>();
-    let journal_dir = match journal_dir(&settings) {
+    let settings = settings::get::<JournalSettings>(cx);
+    let journal_dir = match journal_dir(settings.path.as_ref().unwrap()) {
         Some(journal_dir) => journal_dir,
         None => {
             log::error!("Can't determine journal directory");
@@ -31,8 +68,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
         .join(format!("{:02}", now.month()));
     let entry_path = month_dir.join(format!("{:02}.md", now.day()));
     let now = now.time();
-    let hour_format = &settings.journal_overrides.hour_format;
-    let entry_heading = heading_entry(now, &hour_format);
+    let entry_heading = heading_entry(now, &settings.hour_format);
 
     let create_entry = cx.background().spawn(async move {
         std::fs::create_dir_all(month_dir)?;
@@ -76,14 +112,8 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut AppContext) {
     .detach_and_log_err(cx);
 }
 
-fn journal_dir(settings: &Settings) -> Option<PathBuf> {
-    let journal_dir = settings
-        .journal_overrides
-        .path
-        .as_ref()
-        .unwrap_or(settings.journal_defaults.path.as_ref()?);
-
-    let expanded_journal_dir = shellexpand::full(&journal_dir) //TODO handle this better
+fn journal_dir(path: &str) -> Option<PathBuf> {
+    let expanded_journal_dir = shellexpand::full(path) //TODO handle this better
         .ok()
         .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"));
 

crates/language/Cargo.toml 🔗

@@ -36,16 +36,19 @@ sum_tree = { path = "../sum_tree" }
 text = { path = "../text" }
 theme = { path = "../theme" }
 util = { path = "../util" }
+
 anyhow.workspace = true
 async-broadcast = "0.4"
 async-trait.workspace = true
 futures.workspace = true
+glob.workspace = true
 lazy_static.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 rand = { workspace = true, optional = true }
 regex.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true

crates/language/src/buffer.rs 🔗

@@ -5,6 +5,7 @@ pub use crate::{
 };
 use crate::{
     diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
+    language_settings::{language_settings, LanguageSettings},
     outline::OutlineItem,
     syntax_map::{
         SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint,
@@ -18,7 +19,6 @@ use futures::FutureExt as _;
 use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
-use settings::Settings;
 use similar::{ChangeTag, TextDiff};
 use smallvec::SmallVec;
 use smol::future::yield_now;
@@ -1827,11 +1827,11 @@ impl BufferSnapshot {
 
     pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
         let language_name = self.language_at(position).map(|language| language.name());
-        let settings = cx.global::<Settings>();
-        if settings.hard_tabs(language_name.as_deref()) {
+        let settings = language_settings(language_name.as_deref(), cx);
+        if settings.hard_tabs {
             IndentSize::tab()
         } else {
-            IndentSize::spaces(settings.tab_size(language_name.as_deref()).get())
+            IndentSize::spaces(settings.tab_size.get())
         }
     }
 
@@ -2146,6 +2146,15 @@ impl BufferSnapshot {
             .or(self.language.as_ref())
     }
 
+    pub fn settings_at<'a, D: ToOffset>(
+        &self,
+        position: D,
+        cx: &'a AppContext,
+    ) -> &'a LanguageSettings {
+        let language = self.language_at(position);
+        language_settings(language.map(|l| l.name()).as_deref(), cx)
+    }
+
     pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {
         let offset = position.to_offset(self);
 

crates/language/src/buffer_tests.rs 🔗

@@ -1,3 +1,7 @@
+use crate::language_settings::{
+    AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent,
+};
+
 use super::*;
 use clock::ReplicaId;
 use collections::BTreeMap;
@@ -7,7 +11,7 @@ use indoc::indoc;
 use proto::deserialize_operation;
 use rand::prelude::*;
 use regex::RegexBuilder;
-use settings::Settings;
+use settings::SettingsStore;
 use std::{
     cell::RefCell,
     env,
@@ -36,7 +40,8 @@ fn init_logger() {
 
 #[gpui::test]
 fn test_line_endings(cx: &mut gpui::AppContext) {
-    cx.set_global(Settings::test(cx));
+    init_settings(cx, |_| {});
+
     cx.add_model(|cx| {
         let mut buffer =
             Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx);
@@ -862,8 +867,7 @@ fn test_range_for_syntax_ancestor(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_autoindent_with_soft_tabs(cx: &mut AppContext) {
-    let settings = Settings::test(cx);
-    cx.set_global(settings);
+    init_settings(cx, |_| {});
 
     cx.add_model(|cx| {
         let text = "fn a() {}";
@@ -903,9 +907,9 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
-    let mut settings = Settings::test(cx);
-    settings.editor_overrides.hard_tabs = Some(true);
-    cx.set_global(settings);
+    init_settings(cx, |settings| {
+        settings.defaults.hard_tabs = Some(true);
+    });
 
     cx.add_model(|cx| {
         let text = "fn a() {}";
@@ -945,8 +949,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) {
-    let settings = Settings::test(cx);
-    cx.set_global(settings);
+    init_settings(cx, |_| {});
 
     cx.add_model(|cx| {
         let mut buffer = Buffer::new(
@@ -1082,8 +1085,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC
 
 #[gpui::test]
 fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) {
-    let settings = Settings::test(cx);
-    cx.set_global(settings);
+    init_settings(cx, |_| {});
 
     cx.add_model(|cx| {
         let mut buffer = Buffer::new(
@@ -1145,7 +1147,8 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap
 
 #[gpui::test]
 fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
-    cx.set_global(Settings::test(cx));
+    init_settings(cx, |_| {});
+
     cx.add_model(|cx| {
         let mut buffer = Buffer::new(
             0,
@@ -1201,7 +1204,8 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
-    cx.set_global(Settings::test(cx));
+    init_settings(cx, |_| {});
+
     cx.add_model(|cx| {
         let text = "a\nb";
         let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx);
@@ -1217,7 +1221,8 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
-    cx.set_global(Settings::test(cx));
+    init_settings(cx, |_| {});
+
     cx.add_model(|cx| {
         let text = "
             const a: usize = 1;
@@ -1257,7 +1262,8 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_autoindent_block_mode(cx: &mut AppContext) {
-    cx.set_global(Settings::test(cx));
+    init_settings(cx, |_| {});
+
     cx.add_model(|cx| {
         let text = r#"
             fn a() {
@@ -1339,7 +1345,8 @@ fn test_autoindent_block_mode(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) {
-    cx.set_global(Settings::test(cx));
+    init_settings(cx, |_| {});
+
     cx.add_model(|cx| {
         let text = r#"
             fn a() {
@@ -1417,7 +1424,8 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex
 
 #[gpui::test]
 fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
-    cx.set_global(Settings::test(cx));
+    init_settings(cx, |_| {});
+
     cx.add_model(|cx| {
         let text = "
             * one
@@ -1460,25 +1468,23 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
-    cx.set_global({
-        let mut settings = Settings::test(cx);
-        settings.language_overrides.extend([
+    init_settings(cx, |settings| {
+        settings.languages.extend([
             (
                 "HTML".into(),
-                settings::EditorSettings {
+                LanguageSettingsContent {
                     tab_size: Some(2.try_into().unwrap()),
                     ..Default::default()
                 },
             ),
             (
                 "JavaScript".into(),
-                settings::EditorSettings {
+                LanguageSettingsContent {
                     tab_size: Some(8.try_into().unwrap()),
                     ..Default::default()
                 },
             ),
-        ]);
-        settings
+        ])
     });
 
     let html_language = Arc::new(
@@ -1574,9 +1580,10 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
-    let mut settings = Settings::test(cx);
-    settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
-    cx.set_global(settings);
+    init_settings(cx, |settings| {
+        settings.defaults.tab_size = Some(2.try_into().unwrap());
+    });
+
     cx.add_model(|cx| {
         let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx);
 
@@ -1617,7 +1624,8 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) {
 
 #[gpui::test]
 fn test_language_config_at(cx: &mut AppContext) {
-    cx.set_global(Settings::test(cx));
+    init_settings(cx, |_| {});
+
     cx.add_model(|cx| {
         let language = Language::new(
             LanguageConfig {
@@ -2199,7 +2207,6 @@ fn assert_bracket_pairs(
     language: Language,
     cx: &mut AppContext,
 ) {
-    cx.set_global(Settings::test(cx));
     let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
     let buffer = cx.add_model(|cx| {
         Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx)
@@ -2222,3 +2229,11 @@ fn assert_bracket_pairs(
         bracket_pairs
     );
 }
+
+fn init_settings(cx: &mut AppContext, f: fn(&mut AllLanguageSettingsContent)) {
+    cx.set_global(SettingsStore::test(cx));
+    crate::init(cx);
+    cx.update_global::<SettingsStore, _, _>(|settings, cx| {
+        settings.update_user_settings::<AllLanguageSettings>(cx, f);
+    });
+}

crates/language/src/language.rs 🔗

@@ -1,6 +1,7 @@
 mod buffer;
 mod diagnostic_set;
 mod highlight_map;
+pub mod language_settings;
 mod outline;
 pub mod proto;
 mod syntax_map;
@@ -58,6 +59,10 @@ pub use lsp::LanguageServerId;
 pub use outline::{Outline, OutlineItem};
 pub use tree_sitter::{Parser, Tree};
 
+pub fn init(cx: &mut AppContext) {
+    language_settings::init(cx);
+}
+
 thread_local! {
     static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
 }

crates/language/src/language_settings.rs 🔗

@@ -0,0 +1,337 @@
+use anyhow::Result;
+use collections::HashMap;
+use gpui::AppContext;
+use schemars::{
+    schema::{InstanceType, ObjectValidation, Schema, SchemaObject},
+    JsonSchema,
+};
+use serde::{Deserialize, Serialize};
+use std::{num::NonZeroU32, path::Path, sync::Arc};
+
+pub fn init(cx: &mut AppContext) {
+    settings::register::<AllLanguageSettings>(cx);
+}
+
+pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings {
+    settings::get::<AllLanguageSettings>(cx).language(language)
+}
+
+pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings {
+    settings::get::<AllLanguageSettings>(cx)
+}
+
+#[derive(Debug, Clone)]
+pub struct AllLanguageSettings {
+    pub copilot: CopilotSettings,
+    defaults: LanguageSettings,
+    languages: HashMap<Arc<str>, LanguageSettings>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct LanguageSettings {
+    pub tab_size: NonZeroU32,
+    pub hard_tabs: bool,
+    pub soft_wrap: SoftWrap,
+    pub preferred_line_length: u32,
+    pub format_on_save: FormatOnSave,
+    pub remove_trailing_whitespace_on_save: bool,
+    pub ensure_final_newline_on_save: bool,
+    pub formatter: Formatter,
+    pub enable_language_server: bool,
+    pub show_copilot_suggestions: bool,
+    pub show_whitespaces: ShowWhitespaceSetting,
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct CopilotSettings {
+    pub feature_enabled: bool,
+    pub disabled_globs: Vec<glob::Pattern>,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct AllLanguageSettingsContent {
+    #[serde(default)]
+    pub features: Option<FeaturesContent>,
+    #[serde(default)]
+    pub copilot: Option<CopilotSettingsContent>,
+    #[serde(flatten)]
+    pub defaults: LanguageSettingsContent,
+    #[serde(default, alias = "language_overrides")]
+    pub languages: HashMap<Arc<str>, LanguageSettingsContent>,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct LanguageSettingsContent {
+    #[serde(default)]
+    pub tab_size: Option<NonZeroU32>,
+    #[serde(default)]
+    pub hard_tabs: Option<bool>,
+    #[serde(default)]
+    pub soft_wrap: Option<SoftWrap>,
+    #[serde(default)]
+    pub preferred_line_length: Option<u32>,
+    #[serde(default)]
+    pub format_on_save: Option<FormatOnSave>,
+    #[serde(default)]
+    pub remove_trailing_whitespace_on_save: Option<bool>,
+    #[serde(default)]
+    pub ensure_final_newline_on_save: Option<bool>,
+    #[serde(default)]
+    pub formatter: Option<Formatter>,
+    #[serde(default)]
+    pub enable_language_server: Option<bool>,
+    #[serde(default)]
+    pub show_copilot_suggestions: Option<bool>,
+    #[serde(default)]
+    pub show_whitespaces: Option<ShowWhitespaceSetting>,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct CopilotSettingsContent {
+    #[serde(default)]
+    pub disabled_globs: Option<Vec<String>>,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct FeaturesContent {
+    pub copilot: Option<bool>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SoftWrap {
+    None,
+    EditorWidth,
+    PreferredLineLength,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum FormatOnSave {
+    On,
+    Off,
+    LanguageServer,
+    External {
+        command: Arc<str>,
+        arguments: Arc<[String]>,
+    },
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowWhitespaceSetting {
+    Selection,
+    None,
+    All,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Formatter {
+    LanguageServer,
+    External {
+        command: Arc<str>,
+        arguments: Arc<[String]>,
+    },
+}
+
+impl AllLanguageSettings {
+    pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
+        if let Some(name) = language_name {
+            if let Some(overrides) = self.languages.get(name) {
+                return overrides;
+            }
+        }
+        &self.defaults
+    }
+
+    pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
+        !self
+            .copilot
+            .disabled_globs
+            .iter()
+            .any(|glob| glob.matches_path(path))
+    }
+
+    pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool {
+        if !self.copilot.feature_enabled {
+            return false;
+        }
+
+        if let Some(path) = path {
+            if !self.copilot_enabled_for_path(path) {
+                return false;
+            }
+        }
+
+        self.language(language_name).show_copilot_suggestions
+    }
+}
+
+impl settings::Setting for AllLanguageSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = AllLanguageSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_settings: &[&Self::FileContent],
+        _: &AppContext,
+    ) -> Result<Self> {
+        // A default is provided for all settings.
+        let mut defaults: LanguageSettings =
+            serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
+
+        let mut languages = HashMap::default();
+        for (language_name, settings) in &default_value.languages {
+            let mut language_settings = defaults.clone();
+            merge_settings(&mut language_settings, &settings);
+            languages.insert(language_name.clone(), language_settings);
+        }
+
+        let mut copilot_enabled = default_value
+            .features
+            .as_ref()
+            .and_then(|f| f.copilot)
+            .ok_or_else(Self::missing_default)?;
+        let mut copilot_globs = default_value
+            .copilot
+            .as_ref()
+            .and_then(|c| c.disabled_globs.as_ref())
+            .ok_or_else(Self::missing_default)?;
+
+        for user_settings in user_settings {
+            if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
+                copilot_enabled = copilot;
+            }
+            if let Some(globs) = user_settings
+                .copilot
+                .as_ref()
+                .and_then(|f| f.disabled_globs.as_ref())
+            {
+                copilot_globs = globs;
+            }
+
+            // A user's global settings override the default global settings and
+            // all default language-specific settings.
+            merge_settings(&mut defaults, &user_settings.defaults);
+            for language_settings in languages.values_mut() {
+                merge_settings(language_settings, &user_settings.defaults);
+            }
+
+            // A user's language-specific settings override default language-specific settings.
+            for (language_name, user_language_settings) in &user_settings.languages {
+                merge_settings(
+                    languages
+                        .entry(language_name.clone())
+                        .or_insert_with(|| defaults.clone()),
+                    &user_language_settings,
+                );
+            }
+        }
+
+        Ok(Self {
+            copilot: CopilotSettings {
+                feature_enabled: copilot_enabled,
+                disabled_globs: copilot_globs
+                    .iter()
+                    .filter_map(|pattern| glob::Pattern::new(pattern).ok())
+                    .collect(),
+            },
+            defaults,
+            languages,
+        })
+    }
+
+    fn json_schema(
+        generator: &mut schemars::gen::SchemaGenerator,
+        params: &settings::SettingsJsonSchemaParams,
+        _: &AppContext,
+    ) -> schemars::schema::RootSchema {
+        let mut root_schema = generator.root_schema_for::<Self::FileContent>();
+
+        // Create a schema for a 'languages overrides' object, associating editor
+        // settings with specific langauges.
+        assert!(root_schema
+            .definitions
+            .contains_key("LanguageSettingsContent"));
+
+        let languages_object_schema = SchemaObject {
+            instance_type: Some(InstanceType::Object.into()),
+            object: Some(Box::new(ObjectValidation {
+                properties: params
+                    .language_names
+                    .iter()
+                    .map(|name| {
+                        (
+                            name.clone(),
+                            Schema::new_ref("#/definitions/LanguageSettingsContent".into()),
+                        )
+                    })
+                    .collect(),
+                ..Default::default()
+            })),
+            ..Default::default()
+        };
+
+        root_schema
+            .definitions
+            .extend([("Languages".into(), languages_object_schema.into())]);
+
+        root_schema
+            .schema
+            .object
+            .as_mut()
+            .unwrap()
+            .properties
+            .extend([
+                (
+                    "languages".to_owned(),
+                    Schema::new_ref("#/definitions/Languages".into()),
+                ),
+                // For backward compatibility
+                (
+                    "language_overrides".to_owned(),
+                    Schema::new_ref("#/definitions/Languages".into()),
+                ),
+            ]);
+
+        root_schema
+    }
+}
+
+fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) {
+    merge(&mut settings.tab_size, src.tab_size);
+    merge(&mut settings.hard_tabs, src.hard_tabs);
+    merge(&mut settings.soft_wrap, src.soft_wrap);
+    merge(
+        &mut settings.preferred_line_length,
+        src.preferred_line_length,
+    );
+    merge(&mut settings.formatter, src.formatter.clone());
+    merge(&mut settings.format_on_save, src.format_on_save.clone());
+    merge(
+        &mut settings.remove_trailing_whitespace_on_save,
+        src.remove_trailing_whitespace_on_save,
+    );
+    merge(
+        &mut settings.ensure_final_newline_on_save,
+        src.ensure_final_newline_on_save,
+    );
+    merge(
+        &mut settings.enable_language_server,
+        src.enable_language_server,
+    );
+    merge(
+        &mut settings.show_copilot_suggestions,
+        src.show_copilot_suggestions,
+    );
+    merge(&mut settings.show_whitespaces, src.show_whitespaces);
+
+    fn merge<T>(target: &mut T, value: Option<T>) {
+        if let Some(value) = value {
+            *target = value;
+        }
+    }
+}

crates/language_selector/src/active_buffer_language.rs 🔗

@@ -4,7 +4,6 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
 };
-use settings::Settings;
 use std::sync::Arc;
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
@@ -55,7 +54,7 @@ impl View for ActiveBufferLanguage {
             };
 
             MouseEventHandler::<Self, Self>::new(0, cx, |state, cx| {
-                let theme = &cx.global::<Settings>().theme.workspace.status_bar;
+                let theme = &theme::current(cx).workspace.status_bar;
                 let style = theme.active_language.style_for(state, false);
                 Label::new(active_language_text, style.text.clone())
                     .contained()

crates/language_selector/src/language_selector.rs 🔗

@@ -8,7 +8,6 @@ use gpui::{actions, elements::*, AppContext, ModelHandle, MouseState, ViewContex
 use language::{Buffer, LanguageRegistry};
 use picker::{Picker, PickerDelegate, PickerEvent};
 use project::Project;
-use settings::Settings;
 use std::sync::Arc;
 use util::ResultExt;
 use workspace::Workspace;
@@ -179,8 +178,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
         selected: bool,
         cx: &AppContext,
     ) -> AnyElement<Picker<Self>> {
-        let settings = cx.global::<Settings>();
-        let theme = &settings.theme;
+        let theme = theme::current(cx);
         let mat = &self.matches[ix];
         let style = theme.picker.item.style_for(mouse_state, selected);
         let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());

crates/lsp_log/src/lsp_log.rs 🔗

@@ -13,7 +13,6 @@ use gpui::{
 };
 use language::{Buffer, LanguageServerId, LanguageServerName};
 use project::{Project, WorktreeId};
-use settings::Settings;
 use std::{borrow::Cow, sync::Arc};
 use theme::{ui, Theme};
 use workspace::{
@@ -304,7 +303,7 @@ impl View for LspLogToolbarItemView {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
         let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
         let project = self.project.read(cx);
         let log_view = log_view.read(cx);

crates/outline/Cargo.toml 🔗

@@ -16,7 +16,9 @@ language = { path = "../language" }
 picker = { path = "../picker" }
 settings = { path = "../settings" }
 text = { path = "../text" }
+theme = { path = "../theme" }
 workspace = { path = "../workspace" }
+
 ordered-float.workspace = true
 postage.workspace = true
 smol.workspace = true

crates/outline/src/outline.rs 🔗

@@ -10,7 +10,6 @@ use gpui::{
 use language::Outline;
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::Settings;
 use std::{
     cmp::{self, Reverse},
     sync::Arc,
@@ -34,7 +33,7 @@ pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Worksp
             .buffer()
             .read(cx)
             .snapshot(cx)
-            .outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
+            .outline(Some(theme::current(cx).editor.syntax.as_ref()));
         if let Some(outline) = outline {
             workspace.toggle_modal(cx, |_, cx| {
                 cx.add_view(|cx| {
@@ -204,9 +203,9 @@ impl PickerDelegate for OutlineViewDelegate {
         selected: bool,
         cx: &AppContext,
     ) -> AnyElement<Picker<Self>> {
-        let settings = cx.global::<Settings>();
+        let theme = theme::current(cx);
+        let style = theme.picker.item.style_for(mouse_state, selected);
         let string_match = &self.matches[ix];
-        let style = settings.theme.picker.item.style_for(mouse_state, selected);
         let outline_item = &self.outline.items[string_match.candidate_id];
 
         Text::new(outline_item.text.clone(), style.label.text.clone())

crates/picker/src/picker.rs 🔗

@@ -57,7 +57,7 @@ impl<D: PickerDelegate> View for Picker<D> {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = (self.theme.lock())(&cx.global::<settings::Settings>().theme);
+        let theme = (self.theme.lock())(theme::current(cx).as_ref());
         let query = self.query(cx);
         let match_count = self.delegate.match_count();
 

crates/project/Cargo.toml 🔗

@@ -50,6 +50,7 @@ parking_lot.workspace = true
 postage.workspace = true
 rand.workspace = true
 regex.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
@@ -57,7 +58,7 @@ sha2 = "0.10"
 similar = "1.3"
 smol.workspace = true
 thiserror.workspace = true
-toml = "0.5"
+toml.workspace = true
 itertools = "0.10"
 
 [dev-dependencies]

crates/project/src/project.rs 🔗

@@ -1,6 +1,7 @@
 mod ignore;
 mod lsp_command;
 mod lsp_glob_set;
+mod project_settings;
 pub mod search;
 pub mod terminals;
 pub mod worktree;
@@ -23,6 +24,7 @@ use gpui::{
     ModelHandle, Task, WeakModelHandle,
 };
 use language::{
+    language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter},
     point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -41,10 +43,11 @@ use lsp::{
 use lsp_command::*;
 use lsp_glob_set::LspGlobSet;
 use postage::watch;
+use project_settings::ProjectSettings;
 use rand::prelude::*;
 use search::SearchQuery;
 use serde::Serialize;
-use settings::{FormatOnSave, Formatter, Settings};
+use settings::SettingsStore;
 use sha2::{Digest, Sha256};
 use similar::{ChangeTag, TextDiff};
 use std::{
@@ -64,9 +67,7 @@ use std::{
     },
     time::{Duration, Instant, SystemTime},
 };
-
 use terminals::Terminals;
-
 use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _};
 
 pub use fs::*;
@@ -388,7 +389,13 @@ impl FormatTrigger {
 }
 
 impl Project {
-    pub fn init(client: &Arc<Client>) {
+    pub fn init_settings(cx: &mut AppContext) {
+        settings::register::<ProjectSettings>(cx);
+    }
+
+    pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
+        Self::init_settings(cx);
+
         client.add_model_message_handler(Self::handle_add_collaborator);
         client.add_model_message_handler(Self::handle_update_project_collaborator);
         client.add_model_message_handler(Self::handle_remove_collaborator);
@@ -458,7 +465,9 @@ impl Project {
                 client_state: None,
                 opened_buffer: watch::channel(),
                 client_subscriptions: Vec::new(),
-                _subscriptions: vec![cx.observe_global::<Settings, _>(Self::on_settings_changed)],
+                _subscriptions: vec![
+                    cx.observe_global::<SettingsStore, _>(Self::on_settings_changed)
+                ],
                 _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
                 _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
                 active_entry: None,
@@ -601,12 +610,6 @@ impl Project {
         root_paths: impl IntoIterator<Item = &Path>,
         cx: &mut gpui::TestAppContext,
     ) -> ModelHandle<Project> {
-        if !cx.read(|cx| cx.has_global::<Settings>()) {
-            cx.update(|cx| {
-                cx.set_global(Settings::test(cx));
-            });
-        }
-
         let mut languages = LanguageRegistry::test();
         languages.set_executor(cx.background());
         let http_client = util::http::FakeHttpClient::with_404_response();
@@ -628,7 +631,7 @@ impl Project {
     }
 
     fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
-        let settings = cx.global::<Settings>();
+        let settings = all_language_settings(cx);
 
         let mut language_servers_to_start = Vec::new();
         for buffer in self.opened_buffers.values() {
@@ -636,7 +639,10 @@ impl Project {
                 let buffer = buffer.read(cx);
                 if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language())
                 {
-                    if settings.enable_language_server(Some(&language.name())) {
+                    if settings
+                        .language(Some(&language.name()))
+                        .enable_language_server
+                    {
                         let worktree = file.worktree.read(cx);
                         language_servers_to_start.push((
                             worktree.id(),
@@ -651,7 +657,10 @@ impl Project {
         let mut language_servers_to_stop = Vec::new();
         for language in self.languages.to_vec() {
             for lsp_adapter in language.lsp_adapters() {
-                if !settings.enable_language_server(Some(&language.name())) {
+                if !settings
+                    .language(Some(&language.name()))
+                    .enable_language_server
+                {
                     let lsp_name = &lsp_adapter.name;
                     for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
                         if lsp_name == started_lsp_name {
@@ -2122,7 +2131,7 @@ impl Project {
         let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel();
         let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx);
 
-        let settings_observation = cx.observe_global::<Settings, _>(move |_, _| {
+        let settings_observation = cx.observe_global::<SettingsStore, _>(move |_, _| {
             *settings_changed_tx.borrow_mut() = ();
         });
         cx.spawn_weak(|this, mut cx| async move {
@@ -2199,10 +2208,7 @@ impl Project {
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
-        if !cx
-            .global::<Settings>()
-            .enable_language_server(Some(&language.name()))
-        {
+        if !language_settings(Some(&language.name()), cx).enable_language_server {
             return;
         }
 
@@ -2223,7 +2229,9 @@ impl Project {
                 None => continue,
             };
 
-            let lsp = &cx.global::<Settings>().lsp.get(&adapter.name.0);
+            let lsp = settings::get::<ProjectSettings>(cx)
+                .lsp
+                .get(&adapter.name.0);
             let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
 
             let mut initialization_options = adapter.initialization_options.clone();
@@ -3249,24 +3257,17 @@ impl Project {
 
                 let mut project_transaction = ProjectTransaction::default();
                 for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
-                    let (
-                        format_on_save,
-                        remove_trailing_whitespace,
-                        ensure_final_newline,
-                        formatter,
-                        tab_size,
-                    ) = buffer.read_with(&cx, |buffer, cx| {
-                        let settings = cx.global::<Settings>();
+                    let settings = buffer.read_with(&cx, |buffer, cx| {
                         let language_name = buffer.language().map(|language| language.name());
-                        (
-                            settings.format_on_save(language_name.as_deref()),
-                            settings.remove_trailing_whitespace_on_save(language_name.as_deref()),
-                            settings.ensure_final_newline_on_save(language_name.as_deref()),
-                            settings.formatter(language_name.as_deref()),
-                            settings.tab_size(language_name.as_deref()),
-                        )
+                        language_settings(language_name.as_deref(), cx).clone()
                     });
 
+                    let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
+                    let ensure_final_newline = settings.ensure_final_newline_on_save;
+                    let format_on_save = settings.format_on_save.clone();
+                    let formatter = settings.formatter.clone();
+                    let tab_size = settings.tab_size;
+
                     // First, format buffer's whitespace according to the settings.
                     let trailing_whitespace_diff = if remove_trailing_whitespace {
                         Some(

crates/project/src/project_settings.rs 🔗

@@ -0,0 +1,31 @@
+use collections::HashMap;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+use std::sync::Arc;
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct ProjectSettings {
+    #[serde(default)]
+    pub lsp: HashMap<Arc<str>, LspSettings>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct LspSettings {
+    pub initialization_options: Option<serde_json::Value>,
+}
+
+impl Setting for ProjectSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = Self;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/project/src/project_tests.rs 🔗

@@ -1,10 +1,9 @@
 use crate::{worktree::WorktreeHandle, Event, *};
-use fs::LineEnding;
-use fs::{FakeFs, RealFs};
+use fs::{FakeFs, LineEnding, RealFs};
 use futures::{future, StreamExt};
-use gpui::AppContext;
-use gpui::{executor::Deterministic, test::subscribe};
+use gpui::{executor::Deterministic, test::subscribe, AppContext};
 use language::{
+    language_settings::{AllLanguageSettings, LanguageSettingsContent},
     tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
     OffsetRangeExt, Point, ToPoint,
 };
@@ -26,6 +25,9 @@ fn init_logger() {
 
 #[gpui::test]
 async fn test_symlinks(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+    cx.foreground().allow_parking();
+
     let dir = temp_tree(json!({
         "root": {
             "apple": "",
@@ -65,7 +67,7 @@ async fn test_managing_language_servers(
     deterministic: Arc<Deterministic>,
     cx: &mut gpui::TestAppContext,
 ) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let mut rust_language = Language::new(
         LanguageConfig {
@@ -451,7 +453,7 @@ async fn test_managing_language_servers(
 
 #[gpui::test]
 async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let mut language = Language::new(
         LanguageConfig {
@@ -556,7 +558,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
 
 #[gpui::test]
 async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
@@ -648,7 +650,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
@@ -719,7 +721,7 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let progress_token = "the-progress-token";
     let mut language = Language::new(
@@ -847,7 +849,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let progress_token = "the-progress-token";
     let mut language = Language::new(
@@ -925,7 +927,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
 
 #[gpui::test]
 async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let mut language = Language::new(
         LanguageConfig {
@@ -973,11 +975,8 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
 }
 
 #[gpui::test]
-async fn test_toggling_enable_language_server(
-    deterministic: Arc<Deterministic>,
-    cx: &mut gpui::TestAppContext,
-) {
-    deterministic.forbid_parking();
+async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
 
     let mut rust = Language::new(
         LanguageConfig {
@@ -1051,14 +1050,16 @@ async fn test_toggling_enable_language_server(
 
     // Disable Rust language server, ensuring only that server gets stopped.
     cx.update(|cx| {
-        cx.update_global(|settings: &mut Settings, _| {
-            settings.language_overrides.insert(
-                Arc::from("Rust"),
-                settings::EditorSettings {
-                    enable_language_server: Some(false),
-                    ..Default::default()
-                },
-            );
+        cx.update_global(|settings: &mut SettingsStore, cx| {
+            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.languages.insert(
+                    Arc::from("Rust"),
+                    LanguageSettingsContent {
+                        enable_language_server: Some(false),
+                        ..Default::default()
+                    },
+                );
+            });
         })
     });
     fake_rust_server_1
@@ -1068,21 +1069,23 @@ async fn test_toggling_enable_language_server(
     // Enable Rust and disable JavaScript language servers, ensuring that the
     // former gets started again and that the latter stops.
     cx.update(|cx| {
-        cx.update_global(|settings: &mut Settings, _| {
-            settings.language_overrides.insert(
-                Arc::from("Rust"),
-                settings::EditorSettings {
-                    enable_language_server: Some(true),
-                    ..Default::default()
-                },
-            );
-            settings.language_overrides.insert(
-                Arc::from("JavaScript"),
-                settings::EditorSettings {
-                    enable_language_server: Some(false),
-                    ..Default::default()
-                },
-            );
+        cx.update_global(|settings: &mut SettingsStore, cx| {
+            settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
+                settings.languages.insert(
+                    Arc::from("Rust"),
+                    LanguageSettingsContent {
+                        enable_language_server: Some(true),
+                        ..Default::default()
+                    },
+                );
+                settings.languages.insert(
+                    Arc::from("JavaScript"),
+                    LanguageSettingsContent {
+                        enable_language_server: Some(false),
+                        ..Default::default()
+                    },
+                );
+            });
         })
     });
     let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
@@ -1102,7 +1105,7 @@ async fn test_toggling_enable_language_server(
 
 #[gpui::test]
 async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let mut language = Language::new(
         LanguageConfig {
@@ -1388,7 +1391,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let text = concat!(
         "let one = ;\n", //
@@ -1457,9 +1460,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
-    println!("hello from stdout");
-    eprintln!("hello from stderr");
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let fs = FakeFs::new(cx.background());
     fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
@@ -1515,7 +1516,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
 
 #[gpui::test]
 async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let mut language = Language::new(
         LanguageConfig {
@@ -1673,7 +1674,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let text = "
         use a::b;
@@ -1781,7 +1782,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp
 
 #[gpui::test]
 async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let text = "
         use a::b;
@@ -1902,6 +1903,8 @@ fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
 
 #[gpui::test(iterations = 10)]
 async fn test_definition(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let mut language = Language::new(
         LanguageConfig {
             name: "Rust".into(),
@@ -2001,6 +2004,8 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let mut language = Language::new(
         LanguageConfig {
             name: "TypeScript".into(),
@@ -2085,6 +2090,8 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let mut language = Language::new(
         LanguageConfig {
             name: "TypeScript".into(),
@@ -2138,6 +2145,8 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test(iterations = 10)]
 async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let mut language = Language::new(
         LanguageConfig {
             name: "TypeScript".into(),
@@ -2254,6 +2263,8 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test(iterations = 10)]
 async fn test_save_file(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
         "/dir",
@@ -2284,6 +2295,8 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
         "/dir",
@@ -2313,6 +2326,8 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_save_as(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let fs = FakeFs::new(cx.background());
     fs.insert_tree("/dir", json!({})).await;
 
@@ -2373,6 +2388,9 @@ async fn test_rescan_and_remote_updates(
     deterministic: Arc<Deterministic>,
     cx: &mut gpui::TestAppContext,
 ) {
+    init_test(cx);
+    cx.foreground().allow_parking();
+
     let dir = temp_tree(json!({
         "a": {
             "file1": "",
@@ -2529,6 +2547,8 @@ async fn test_buffer_identity_across_renames(
     deterministic: Arc<Deterministic>,
     cx: &mut gpui::TestAppContext,
 ) {
+    init_test(cx);
+
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
         "/dir",
@@ -2577,6 +2597,8 @@ async fn test_buffer_identity_across_renames(
 
 #[gpui::test]
 async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
         "/dir",
@@ -2621,6 +2643,8 @@ async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
         "/dir",
@@ -2765,6 +2789,8 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let initial_contents = "aaa\nbbbbb\nc\n";
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
@@ -2844,6 +2870,8 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
         "/dir",
@@ -2904,7 +2932,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
@@ -3146,7 +3174,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_rename(cx: &mut gpui::TestAppContext) {
-    cx.foreground().forbid_parking();
+    init_test(cx);
 
     let mut language = Language::new(
         LanguageConfig {
@@ -3284,6 +3312,8 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_search(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let fs = FakeFs::new(cx.background());
     fs.insert_tree(
         "/dir",
@@ -3339,6 +3369,8 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let search_query = "file";
 
     let fs = FakeFs::new(cx.background());
@@ -3447,6 +3479,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let search_query = "file";
 
     let fs = FakeFs::new(cx.background());
@@ -3554,6 +3588,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
 
 #[gpui::test]
 async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
     let search_query = "file";
 
     let fs = FakeFs::new(cx.background());
@@ -3680,3 +3716,13 @@ async fn search(
         })
         .collect())
 }
+
+fn init_test(cx: &mut gpui::TestAppContext) {
+    cx.foreground().forbid_parking();
+
+    cx.update(|cx| {
+        cx.set_global(SettingsStore::test(cx));
+        language::init(cx);
+        Project::init_settings(cx);
+    });
+}

crates/project/src/terminals.rs 🔗

@@ -1,10 +1,7 @@
-use std::path::PathBuf;
-
-use gpui::{ModelContext, ModelHandle, WeakModelHandle};
-use settings::Settings;
-use terminal::{Terminal, TerminalBuilder};
-
 use crate::Project;
+use gpui::{ModelContext, ModelHandle, WeakModelHandle};
+use std::path::PathBuf;
+use terminal::{Terminal, TerminalBuilder, TerminalSettings};
 
 pub struct Terminals {
     pub(crate) local_handles: Vec<WeakModelHandle<terminal::Terminal>>,
@@ -22,17 +19,14 @@ impl Project {
                 "creating terminals as a guest is not supported yet"
             ));
         } else {
-            let settings = cx.global::<Settings>();
-            let shell = settings.terminal_shell();
-            let envs = settings.terminal_env();
-            let scroll = settings.terminal_scroll();
+            let settings = settings::get::<TerminalSettings>(cx);
 
             let terminal = TerminalBuilder::new(
                 working_directory.clone(),
-                shell,
-                envs,
-                settings.terminal_overrides.blinking.clone(),
-                scroll,
+                settings.shell.clone(),
+                settings.env.clone(),
+                Some(settings.blinking.clone()),
+                settings.alternate_scroll,
                 window_id,
             )
             .map(|builder| {

crates/project_panel/Cargo.toml 🔗

@@ -24,6 +24,8 @@ futures.workspace = true
 unicase = "2.6"
 
 [dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 workspace = { path = "../workspace", features = ["test-support"] }

crates/project_panel/src/project_panel.rs 🔗

@@ -20,7 +20,6 @@ use project::{
     repository::GitFileStatus, Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree,
     WorktreeId,
 };
-use settings::Settings;
 use std::{
     cmp::Ordering,
     collections::{hash_map, HashMap},
@@ -1113,7 +1112,7 @@ impl ProjectPanel {
                 ComponentHost::new(FileName::new(
                     details.filename.clone(),
                     details.git_status,
-                    FileName::style(style.text.clone(), &cx.global::<Settings>().theme),
+                    FileName::style(style.text.clone(), &theme::current(cx)),
                 ))
                 .contained()
                 .with_margin_left(style.icon_spacing)
@@ -1223,7 +1222,7 @@ impl ProjectPanel {
             let row_container_style = theme.dragged_entry.container;
 
             move |_, cx: &mut ViewContext<Workspace>| {
-                let theme = cx.global::<Settings>().theme.clone();
+                let theme = theme::current(cx).clone();
                 Self::render_entry_visual_element(
                     &details,
                     None,
@@ -1246,7 +1245,7 @@ impl View for ProjectPanel {
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
         enum ProjectPanel {}
-        let theme = &cx.global::<Settings>().theme.project_panel;
+        let theme = &theme::current(cx).project_panel;
         let mut container_style = theme.container;
         let padding = std::mem::take(&mut container_style.padding);
         let last_worktree_root_id = self.last_worktree_root_id;
@@ -1265,7 +1264,7 @@ impl View for ProjectPanel {
                                 .sum(),
                             cx,
                             move |this, range, items, cx| {
-                                let theme = cx.global::<Settings>().theme.clone();
+                                let theme = theme::current(cx).clone();
                                 let mut dragged_entry_destination =
                                     this.dragged_entry_destination.clone();
                                 this.for_each_visible_entry(range, cx, |id, details, cx| {
@@ -1302,8 +1301,7 @@ impl View for ProjectPanel {
                 .with_child(
                     MouseEventHandler::<Self, _>::new(2, cx, {
                         let button_style = theme.open_project_button.clone();
-                        let context_menu_item_style =
-                            cx.global::<Settings>().theme.context_menu.item.clone();
+                        let context_menu_item_style = theme::current(cx).context_menu.item.clone();
                         move |state, cx| {
                             let button_style = button_style.style_for(state, false).clone();
                             let context_menu_item =
@@ -1378,15 +1376,12 @@ mod tests {
     use gpui::{TestAppContext, ViewHandle};
     use project::FakeFs;
     use serde_json::json;
+    use settings::SettingsStore;
     use std::{collections::HashSet, path::Path};
 
     #[gpui::test]
     async fn test_visible_list(cx: &mut gpui::TestAppContext) {
-        cx.foreground().forbid_parking();
-        cx.update(|cx| {
-            let settings = Settings::test(cx);
-            cx.set_global(settings);
-        });
+        init_test(cx);
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -1474,11 +1469,7 @@ mod tests {
 
     #[gpui::test(iterations = 30)]
     async fn test_editing_files(cx: &mut gpui::TestAppContext) {
-        cx.foreground().forbid_parking();
-        cx.update(|cx| {
-            let settings = Settings::test(cx);
-            cx.set_global(settings);
-        });
+        init_test(cx);
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -1794,11 +1785,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
-        cx.foreground().forbid_parking();
-        cx.update(|cx| {
-            let settings = Settings::test(cx);
-            cx.set_global(settings);
-        });
+        init_test(cx);
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -1958,4 +1945,15 @@ mod tests {
 
         result
     }
+
+    fn init_test(cx: &mut TestAppContext) {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            language::init(cx);
+            editor::init_settings(cx);
+            workspace::init_settings(cx);
+        });
+    }
 }

crates/project_symbols/Cargo.toml 🔗

@@ -17,7 +17,9 @@ project = { path = "../project" }
 text = { path = "../text" }
 settings = { path = "../settings" }
 workspace = { path = "../workspace" }
+theme = { path = "../theme" }
 util = { path = "../util" }
+
 anyhow.workspace = true
 ordered-float.workspace = true
 postage.workspace = true
@@ -30,3 +32,5 @@ gpui = { path = "../gpui", features = ["test-support"] }
 language = { path = "../language", features = ["test-support"] }
 lsp = { path = "../lsp", features = ["test-support"] }
 project = { path = "../project", features = ["test-support"] }
+theme = { path = "../theme", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }

crates/project_symbols/src/project_symbols.rs 🔗

@@ -9,7 +9,6 @@ use gpui::{
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate, PickerEvent};
 use project::{Project, Symbol};
-use settings::Settings;
 use std::{borrow::Cow, cmp::Reverse, sync::Arc};
 use util::ResultExt;
 use workspace::Workspace;
@@ -195,12 +194,13 @@ impl PickerDelegate for ProjectSymbolsDelegate {
         selected: bool,
         cx: &AppContext,
     ) -> AnyElement<Picker<Self>> {
-        let string_match = &self.matches[ix];
-        let settings = cx.global::<Settings>();
-        let style = &settings.theme.picker.item;
+        let theme = theme::current(cx);
+        let style = &theme.picker.item;
         let current_style = style.style_for(mouse_state, selected);
+
+        let string_match = &self.matches[ix];
         let symbol = &self.symbols[string_match.candidate_id];
-        let syntax_runs = styled_runs_for_code_label(&symbol.label, &settings.theme.editor.syntax);
+        let syntax_runs = styled_runs_for_code_label(&symbol.label, &theme.editor.syntax);
 
         let mut path = symbol.path.path.to_string_lossy();
         if self.show_worktree_root_name {
@@ -244,12 +244,12 @@ mod tests {
     use gpui::{serde_json::json, TestAppContext};
     use language::{FakeLspAdapter, Language, LanguageConfig};
     use project::FakeFs;
+    use settings::SettingsStore;
     use std::{path::Path, sync::Arc};
 
     #[gpui::test]
     async fn test_project_symbols(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        cx.update(|cx| cx.set_global(Settings::test(cx)));
+        init_test(cx);
 
         let mut language = Language::new(
             LanguageConfig {
@@ -368,6 +368,17 @@ mod tests {
         });
     }
 
+    fn init_test(cx: &mut TestAppContext) {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            language::init(cx);
+            Project::init_settings(cx);
+            workspace::init_settings(cx);
+        });
+    }
+
     fn symbol(name: &str, path: impl AsRef<Path>) -> lsp::SymbolInformation {
         #[allow(deprecated)]
         lsp::SymbolInformation {

crates/recent_projects/Cargo.toml 🔗

@@ -18,6 +18,7 @@ picker = { path = "../picker" }
 settings = { path = "../settings" }
 text = { path = "../text" }
 util = { path = "../util"}
+theme = { path = "../theme" }
 workspace = { path = "../workspace" }
 
 ordered-float.workspace = true

crates/recent_projects/src/recent_projects.rs 🔗

@@ -10,7 +10,6 @@ use gpui::{
 use highlighted_workspace_location::HighlightedWorkspaceLocation;
 use ordered_float::OrderedFloat;
 use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::Settings;
 use std::sync::Arc;
 use workspace::{
     notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
@@ -173,9 +172,10 @@ impl PickerDelegate for RecentProjectsDelegate {
         selected: bool,
         cx: &gpui::AppContext,
     ) -> AnyElement<Picker<Self>> {
-        let settings = cx.global::<Settings>();
+        let theme = theme::current(cx);
+        let style = theme.picker.item.style_for(mouse_state, selected);
+
         let string_match = &self.matches[ix];
-        let style = settings.theme.picker.item.style_for(mouse_state, selected);
 
         let highlighted_location = HighlightedWorkspaceLocation::new(
             &string_match,

crates/search/Cargo.toml 🔗

@@ -30,6 +30,7 @@ smol.workspace = true
 glob.workspace = true
 
 [dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
 gpui = { path = "../gpui", features = ["test-support"] }
 serde_json.workspace = true

crates/search/src/buffer_search.rs 🔗

@@ -13,7 +13,6 @@ use gpui::{
 };
 use project::search::SearchQuery;
 use serde::Deserialize;
-use settings::Settings;
 use std::{any::Any, sync::Arc};
 use util::ResultExt;
 use workspace::{
@@ -93,7 +92,7 @@ impl View for BufferSearchBar {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
         let editor_container = if self.query_contains_error {
             theme.search.invalid_editor
         } else {
@@ -324,16 +323,12 @@ impl BufferSearchBar {
             return None;
         }
 
-        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+        let tooltip_style = theme::current(cx).tooltip.clone();
         let is_active = self.is_search_option_enabled(option);
         Some(
             MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
-                let style = cx
-                    .global::<Settings>()
-                    .theme
-                    .search
-                    .option_button
-                    .style_for(state, is_active);
+                let theme = theme::current(cx);
+                let style = theme.search.option_button.style_for(state, is_active);
                 Label::new(icon, style.text.clone())
                     .contained()
                     .with_style(style.container)
@@ -371,16 +366,12 @@ impl BufferSearchBar {
                 tooltip = "Select Next Match";
             }
         };
-        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+        let tooltip_style = theme::current(cx).tooltip.clone();
 
         enum NavButton {}
         MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
-            let style = cx
-                .global::<Settings>()
-                .theme
-                .search
-                .option_button
-                .style_for(state, false);
+            let theme = theme::current(cx);
+            let style = theme.search.option_button.style_for(state, false);
             Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -408,7 +399,7 @@ impl BufferSearchBar {
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
         let tooltip = "Dismiss Buffer Search";
-        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+        let tooltip_style = theme::current(cx).tooltip.clone();
 
         enum CloseButton {}
         MouseEventHandler::<CloseButton, _>::new(0, cx, |state, _| {
@@ -655,19 +646,11 @@ mod tests {
     use editor::{DisplayPoint, Editor};
     use gpui::{color::Color, test::EmptyView, TestAppContext};
     use language::Buffer;
-    use std::sync::Arc;
     use unindent::Unindent as _;
 
     #[gpui::test]
     async fn test_search_simple(cx: &mut TestAppContext) {
-        let fonts = cx.font_cache();
-        let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
-        theme.search.match_background = Color::red();
-        cx.update(|cx| {
-            let mut settings = Settings::test(cx);
-            settings.theme = Arc::new(theme);
-            cx.set_global(settings)
-        });
+        crate::project_search::tests::init_test(cx);
 
         let buffer = cx.add_model(|cx| {
             Buffer::new(

crates/search/src/project_search.rs 🔗

@@ -17,7 +17,6 @@ use gpui::{
 };
 use menu::Confirm;
 use project::{search::SearchQuery, Project};
-use settings::Settings;
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -195,7 +194,7 @@ impl View for ProjectSearchView {
         if model.match_ranges.is_empty() {
             enum Status {}
 
-            let theme = cx.global::<Settings>().theme.clone();
+            let theme = theme::current(cx).clone();
             let text = if self.query_editor.read(cx).text(cx).is_empty() {
                 ""
             } else if model.pending_search.is_some() {
@@ -903,16 +902,12 @@ impl ProjectSearchBar {
                 tooltip = "Select Next Match";
             }
         };
-        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+        let tooltip_style = theme::current(cx).tooltip.clone();
 
         enum NavButton {}
         MouseEventHandler::<NavButton, _>::new(direction as usize, cx, |state, cx| {
-            let style = &cx
-                .global::<Settings>()
-                .theme
-                .search
-                .option_button
-                .style_for(state, false);
+            let theme = theme::current(cx);
+            let style = theme.search.option_button.style_for(state, false);
             Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -939,15 +934,11 @@ impl ProjectSearchBar {
         option: SearchOption,
         cx: &mut ViewContext<Self>,
     ) -> AnyElement<Self> {
-        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+        let tooltip_style = theme::current(cx).tooltip.clone();
         let is_active = self.is_option_enabled(option, cx);
         MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
-            let style = &cx
-                .global::<Settings>()
-                .theme
-                .search
-                .option_button
-                .style_for(state, is_active);
+            let theme = theme::current(cx);
+            let style = theme.search.option_button.style_for(state, is_active);
             Label::new(icon, style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -992,7 +983,7 @@ impl View for ProjectSearchBar {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         if let Some(search) = self.active_project_search.as_ref() {
             let search = search.read(cx);
-            let theme = cx.global::<Settings>().theme.clone();
+            let theme = theme::current(cx).clone();
             let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
                 theme.search.invalid_editor
             } else {
@@ -1146,25 +1137,19 @@ impl ToolbarItemView for ProjectSearchBar {
 }
 
 #[cfg(test)]
-mod tests {
+pub mod tests {
     use super::*;
     use editor::DisplayPoint;
     use gpui::{color::Color, executor::Deterministic, TestAppContext};
     use project::FakeFs;
     use serde_json::json;
+    use settings::SettingsStore;
     use std::sync::Arc;
+    use theme::ThemeSettings;
 
     #[gpui::test]
     async fn test_project_search(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-        let fonts = cx.font_cache();
-        let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
-        theme.search.match_background = Color::red();
-        cx.update(|cx| {
-            let mut settings = Settings::test(cx);
-            settings.theme = Arc::new(theme);
-            cx.set_global(settings);
-            cx.set_global(ActiveSearches::default());
-        });
+        init_test(cx);
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -1279,4 +1264,27 @@ mod tests {
             );
         });
     }
+
+    pub fn init_test(cx: &mut TestAppContext) {
+        let fonts = cx.font_cache();
+        let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
+        theme.search.match_background = Color::red();
+
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            cx.set_global(ActiveSearches::default());
+
+            theme::init((), cx);
+            cx.update_global::<SettingsStore, _, _>(|store, _| {
+                let mut settings = store.get::<ThemeSettings>(None).clone();
+                settings.theme = Arc::new(theme);
+                store.override_global(settings)
+            });
+
+            language::init(cx);
+            client::init_settings(cx);
+            editor::init_settings(cx);
+            workspace::init_settings(cx);
+        });
+    }
 }

crates/settings/Cargo.toml 🔗

@@ -9,7 +9,7 @@ path = "src/settings.rs"
 doctest = false
 
 [features]
-test-support = []
+test-support = ["gpui/test-support", "fs/test-support"]
 
 [dependencies]
 assets = { path = "../assets" }
@@ -17,21 +17,21 @@ collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 sqlez = { path = "../sqlez" }
 fs = { path = "../fs" }
-anyhow.workspace = true
-futures.workspace = true
-theme = { path = "../theme" }
 staff_mode = { path = "../staff_mode" }
 util = { path = "../util" }
 
+anyhow.workspace = true
+futures.workspace = true
 glob.workspace = true
 json_comments = "0.2"
 lazy_static.workspace = true
 postage.workspace = true
-schemars = "0.8"
+schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-toml = "0.5"
+smallvec.workspace = true
+toml.workspace = true
 tree-sitter = "*"
 tree-sitter-json = "*"
 

crates/settings/src/keymap_file.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{parse_json_with_comments, Settings};
+use crate::settings_store::parse_json_with_comments;
 use anyhow::{Context, Result};
 use assets::Assets;
 use collections::BTreeMap;
@@ -41,20 +41,14 @@ impl JsonSchema for KeymapAction {
 struct ActionWithData(Box<str>, Box<RawValue>);
 
 impl KeymapFileContent {
-    pub fn load_defaults(cx: &mut AppContext) {
-        for path in ["keymaps/default.json", "keymaps/vim.json"] {
-            Self::load(path, cx).unwrap();
-        }
-
-        if let Some(asset_path) = cx.global::<Settings>().base_keymap.asset_path() {
-            Self::load(asset_path, cx).log_err();
-        }
-    }
-
-    pub fn load(asset_path: &str, cx: &mut AppContext) -> Result<()> {
+    pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
         let content = Assets::get(asset_path).unwrap().data;
         let content_str = std::str::from_utf8(content.as_ref()).unwrap();
-        parse_json_with_comments::<Self>(content_str)?.add_to_cx(cx)
+        Self::parse(content_str)?.add_to_cx(cx)
+    }
+
+    pub fn parse(content: &str) -> Result<Self> {
+        parse_json_with_comments::<Self>(content)
     }
 
     pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {

crates/settings/src/settings.rs 🔗

@@ -1,1614 +1,19 @@
 mod keymap_file;
-pub mod settings_file;
-pub mod watched_json;
-
-use anyhow::{bail, Result};
-use gpui::{
-    font_cache::{FamilyId, FontCache},
-    fonts, AssetSource,
-};
-use lazy_static::lazy_static;
-use schemars::{
-    gen::{SchemaGenerator, SchemaSettings},
-    schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
-    JsonSchema,
-};
-use serde::{de::DeserializeOwned, Deserialize, Serialize};
-use serde_json::Value;
-use sqlez::{
-    bindable::{Bind, Column, StaticColumnCount},
-    statement::Statement,
-};
-use std::{
-    borrow::Cow, collections::HashMap, num::NonZeroU32, ops::Range, path::Path, str, sync::Arc,
-};
-use theme::{Theme, ThemeRegistry};
-use tree_sitter::{Query, Tree};
-use util::{RangeExt, ResultExt as _};
+mod settings_file;
+mod settings_store;
 
+use gpui::AssetSource;
 pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
-pub use watched_json::watch_files;
+pub use settings_file::*;
+pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
+use std::{borrow::Cow, str};
 
 pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
 pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
 
-#[derive(Clone)]
-pub struct Settings {
-    pub features: Features,
-    pub buffer_font_family_name: String,
-    pub buffer_font_features: fonts::Features,
-    pub buffer_font_family: FamilyId,
-    pub default_buffer_font_size: f32,
-    pub buffer_font_size: f32,
-    pub active_pane_magnification: f32,
-    pub cursor_blink: bool,
-    pub confirm_quit: bool,
-    pub hover_popover_enabled: bool,
-    pub show_completions_on_input: bool,
-    pub show_call_status_icon: bool,
-    pub show_scrollbars: ShowScrollbars,
-    pub vim_mode: bool,
-    pub autosave: Autosave,
-    pub default_dock_anchor: DockAnchor,
-    pub editor_defaults: EditorSettings,
-    pub editor_overrides: EditorSettings,
-    pub git: GitSettings,
-    pub git_overrides: GitSettings,
-    pub copilot: CopilotSettings,
-    pub journal_defaults: JournalSettings,
-    pub journal_overrides: JournalSettings,
-    pub terminal_defaults: TerminalSettings,
-    pub terminal_overrides: TerminalSettings,
-    pub language_defaults: HashMap<Arc<str>, EditorSettings>,
-    pub language_overrides: HashMap<Arc<str>, EditorSettings>,
-    pub lsp: HashMap<Arc<str>, LspSettings>,
-    pub theme: Arc<Theme>,
-    pub telemetry_defaults: TelemetrySettings,
-    pub telemetry_overrides: TelemetrySettings,
-    pub auto_update: bool,
-    pub base_keymap: BaseKeymap,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowScrollbars {
-    #[default]
-    Auto,
-    System,
-    Always,
-    Never,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
-pub enum BaseKeymap {
-    #[default]
-    VSCode,
-    JetBrains,
-    SublimeText,
-    Atom,
-    TextMate,
-}
-
-impl BaseKeymap {
-    pub const OPTIONS: [(&'static str, Self); 5] = [
-        ("VSCode (Default)", Self::VSCode),
-        ("Atom", Self::Atom),
-        ("JetBrains", Self::JetBrains),
-        ("Sublime Text", Self::SublimeText),
-        ("TextMate", Self::TextMate),
-    ];
-
-    pub fn asset_path(&self) -> Option<&'static str> {
-        match self {
-            BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
-            BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
-            BaseKeymap::Atom => Some("keymaps/atom.json"),
-            BaseKeymap::TextMate => Some("keymaps/textmate.json"),
-            BaseKeymap::VSCode => None,
-        }
-    }
-
-    pub fn names() -> impl Iterator<Item = &'static str> {
-        Self::OPTIONS.iter().map(|(name, _)| *name)
-    }
-
-    pub fn from_names(option: &str) -> BaseKeymap {
-        Self::OPTIONS
-            .iter()
-            .copied()
-            .find_map(|(name, value)| (name == option).then(|| value))
-            .unwrap_or_default()
-    }
-}
-
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct TelemetrySettings {
-    diagnostics: Option<bool>,
-    metrics: Option<bool>,
-}
-
-impl TelemetrySettings {
-    pub fn metrics(&self) -> bool {
-        self.metrics.unwrap()
-    }
-
-    pub fn diagnostics(&self) -> bool {
-        self.diagnostics.unwrap()
-    }
-
-    pub fn set_metrics(&mut self, value: bool) {
-        self.metrics = Some(value);
-    }
-
-    pub fn set_diagnostics(&mut self, value: bool) {
-        self.diagnostics = Some(value);
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct CopilotSettings {
-    pub disabled_globs: Vec<glob::Pattern>,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct CopilotSettingsContent {
-    #[serde(default)]
-    pub disabled_globs: Option<Vec<String>>,
-}
-
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct GitSettings {
-    pub git_gutter: Option<GitGutter>,
-    pub gutter_debounce: Option<u64>,
-}
-
-#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum GitGutter {
-    #[default]
-    TrackedFiles,
-    Hide,
-}
-
-pub struct GitGutterConfig {}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct EditorSettings {
-    pub tab_size: Option<NonZeroU32>,
-    pub hard_tabs: Option<bool>,
-    pub soft_wrap: Option<SoftWrap>,
-    pub preferred_line_length: Option<u32>,
-    pub format_on_save: Option<FormatOnSave>,
-    pub remove_trailing_whitespace_on_save: Option<bool>,
-    pub ensure_final_newline_on_save: Option<bool>,
-    pub formatter: Option<Formatter>,
-    pub enable_language_server: Option<bool>,
-    pub show_copilot_suggestions: Option<bool>,
-    pub show_whitespaces: Option<ShowWhitespaces>,
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum SoftWrap {
-    None,
-    EditorWidth,
-    PreferredLineLength,
-}
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum FormatOnSave {
-    On,
-    Off,
-    LanguageServer,
-    External {
-        command: String,
-        arguments: Vec<String>,
-    },
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Formatter {
-    LanguageServer,
-    External {
-        command: String,
-        arguments: Vec<String>,
-    },
-}
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Autosave {
-    Off,
-    AfterDelay { milliseconds: u64 },
-    OnFocusChange,
-    OnWindowChange,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-pub struct JournalSettings {
-    pub path: Option<String>,
-    pub hour_format: Option<HourFormat>,
-}
-
-impl Default for JournalSettings {
-    fn default() -> Self {
-        Self {
-            path: Some("~".into()),
-            hour_format: Some(Default::default()),
-        }
-    }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum HourFormat {
-    Hour12,
-    Hour24,
-}
-
-impl Default for HourFormat {
-    fn default() -> Self {
-        Self::Hour12
-    }
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct TerminalSettings {
-    pub shell: Option<Shell>,
-    pub working_directory: Option<WorkingDirectory>,
-    pub font_size: Option<f32>,
-    pub font_family: Option<String>,
-    pub line_height: Option<TerminalLineHeight>,
-    pub font_features: Option<fonts::Features>,
-    pub env: Option<HashMap<String, String>>,
-    pub blinking: Option<TerminalBlink>,
-    pub alternate_scroll: Option<AlternateScroll>,
-    pub option_as_meta: Option<bool>,
-    pub copy_on_select: Option<bool>,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
-#[serde(rename_all = "snake_case")]
-pub enum TerminalLineHeight {
-    #[default]
-    Comfortable,
-    Standard,
-    Custom(f32),
-}
-
-impl TerminalLineHeight {
-    fn value(&self) -> f32 {
-        match self {
-            TerminalLineHeight::Comfortable => 1.618,
-            TerminalLineHeight::Standard => 1.3,
-            TerminalLineHeight::Custom(line_height) => *line_height,
-        }
-    }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum TerminalBlink {
-    Off,
-    TerminalControlled,
-    On,
-}
-
-impl Default for TerminalBlink {
-    fn default() -> Self {
-        TerminalBlink::TerminalControlled
-    }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum Shell {
-    System,
-    Program(String),
-    WithArguments { program: String, args: Vec<String> },
-}
-
-impl Default for Shell {
-    fn default() -> Self {
-        Shell::System
-    }
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum AlternateScroll {
-    On,
-    Off,
-}
-
-impl Default for AlternateScroll {
-    fn default() -> Self {
-        AlternateScroll::On
-    }
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum WorkingDirectory {
-    CurrentProjectDirectory,
-    FirstProjectDirectory,
-    AlwaysHome,
-    Always { directory: String },
-}
-
-impl Default for WorkingDirectory {
-    fn default() -> Self {
-        Self::CurrentProjectDirectory
-    }
-}
-
-impl TerminalSettings {
-    fn line_height(&self) -> Option<f32> {
-        self.line_height
-            .to_owned()
-            .map(|line_height| line_height.value())
-    }
-}
-
-#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum DockAnchor {
-    #[default]
-    Bottom,
-    Right,
-    Expanded,
-}
-
-impl StaticColumnCount for DockAnchor {}
-impl Bind for DockAnchor {
-    fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
-        match self {
-            DockAnchor::Bottom => "Bottom",
-            DockAnchor::Right => "Right",
-            DockAnchor::Expanded => "Expanded",
-        }
-        .bind(statement, start_index)
-    }
-}
-
-impl Column for DockAnchor {
-    fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
-        String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
-            Ok((
-                match anchor_text.as_ref() {
-                    "Bottom" => DockAnchor::Bottom,
-                    "Right" => DockAnchor::Right,
-                    "Expanded" => DockAnchor::Expanded,
-                    _ => bail!("Stored dock anchor is incorrect"),
-                },
-                next_index,
-            ))
-        })
-    }
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
-pub struct SettingsFileContent {
-    #[serde(default)]
-    pub buffer_font_family: Option<String>,
-    #[serde(default)]
-    pub buffer_font_size: Option<f32>,
-    #[serde(default)]
-    pub buffer_font_features: Option<fonts::Features>,
-    #[serde(default)]
-    pub copilot: Option<CopilotSettingsContent>,
-    #[serde(default)]
-    pub active_pane_magnification: Option<f32>,
-    #[serde(default)]
-    pub show_scrollbars: Option<ShowScrollbars>,
-    #[serde(default)]
-    pub cursor_blink: Option<bool>,
-    #[serde(default)]
-    pub confirm_quit: Option<bool>,
-    #[serde(default)]
-    pub hover_popover_enabled: Option<bool>,
-    #[serde(default)]
-    pub show_completions_on_input: Option<bool>,
-    #[serde(default)]
-    pub show_call_status_icon: Option<bool>,
-    #[serde(default)]
-    pub vim_mode: Option<bool>,
-    #[serde(default)]
-    pub autosave: Option<Autosave>,
-    #[serde(default)]
-    pub default_dock_anchor: Option<DockAnchor>,
-    #[serde(flatten)]
-    pub editor: EditorSettings,
-    #[serde(default)]
-    pub journal: JournalSettings,
-    #[serde(default)]
-    pub terminal: TerminalSettings,
-    #[serde(default)]
-    pub git: Option<GitSettings>,
-    #[serde(default)]
-    #[serde(alias = "language_overrides")]
-    pub languages: HashMap<Arc<str>, EditorSettings>,
-    #[serde(default)]
-    pub lsp: HashMap<Arc<str>, LspSettings>,
-    #[serde(default)]
-    pub theme: Option<String>,
-    #[serde(default)]
-    pub telemetry: TelemetrySettings,
-    #[serde(default)]
-    pub auto_update: Option<bool>,
-    #[serde(default)]
-    pub base_keymap: Option<BaseKeymap>,
-    #[serde(default)]
-    pub features: FeaturesContent,
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub struct LspSettings {
-    pub initialization_options: Option<Value>,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Features {
-    pub copilot: bool,
-}
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub struct FeaturesContent {
-    pub copilot: Option<bool>,
-}
-
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ShowWhitespaces {
-    #[default]
-    Selection,
-    None,
-    All,
-}
-
-impl Settings {
-    pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
-        match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
-            Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
-            Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
-        }
-    }
-
-    /// Fill out the settings corresponding to the default.json file, overrides will be set later
-    pub fn defaults(
-        assets: impl AssetSource,
-        font_cache: &FontCache,
-        themes: &ThemeRegistry,
-    ) -> Self {
-        #[track_caller]
-        fn required<T>(value: Option<T>) -> Option<T> {
-            assert!(value.is_some(), "missing default setting value");
-            value
-        }
-
-        let defaults: SettingsFileContent = parse_json_with_comments(
-            str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
-        )
-        .unwrap();
-
-        let buffer_font_features = defaults.buffer_font_features.unwrap();
-        Self {
-            buffer_font_family: font_cache
-                .load_family(
-                    &[defaults.buffer_font_family.as_ref().unwrap()],
-                    &buffer_font_features,
-                )
-                .unwrap(),
-            buffer_font_family_name: defaults.buffer_font_family.unwrap(),
-            buffer_font_features,
-            buffer_font_size: defaults.buffer_font_size.unwrap(),
-            active_pane_magnification: defaults.active_pane_magnification.unwrap(),
-            default_buffer_font_size: defaults.buffer_font_size.unwrap(),
-            confirm_quit: defaults.confirm_quit.unwrap(),
-            cursor_blink: defaults.cursor_blink.unwrap(),
-            hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
-            show_completions_on_input: defaults.show_completions_on_input.unwrap(),
-            show_call_status_icon: defaults.show_call_status_icon.unwrap(),
-            vim_mode: defaults.vim_mode.unwrap(),
-            autosave: defaults.autosave.unwrap(),
-            default_dock_anchor: defaults.default_dock_anchor.unwrap(),
-            editor_defaults: EditorSettings {
-                tab_size: required(defaults.editor.tab_size),
-                hard_tabs: required(defaults.editor.hard_tabs),
-                soft_wrap: required(defaults.editor.soft_wrap),
-                preferred_line_length: required(defaults.editor.preferred_line_length),
-                remove_trailing_whitespace_on_save: required(
-                    defaults.editor.remove_trailing_whitespace_on_save,
-                ),
-                ensure_final_newline_on_save: required(
-                    defaults.editor.ensure_final_newline_on_save,
-                ),
-                format_on_save: required(defaults.editor.format_on_save),
-                formatter: required(defaults.editor.formatter),
-                enable_language_server: required(defaults.editor.enable_language_server),
-                show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
-                show_whitespaces: required(defaults.editor.show_whitespaces),
-            },
-            editor_overrides: Default::default(),
-            copilot: CopilotSettings {
-                disabled_globs: defaults
-                    .copilot
-                    .unwrap()
-                    .disabled_globs
-                    .unwrap()
-                    .into_iter()
-                    .map(|s| glob::Pattern::new(&s).unwrap())
-                    .collect(),
-            },
-            git: defaults.git.unwrap(),
-            git_overrides: Default::default(),
-            journal_defaults: defaults.journal,
-            journal_overrides: Default::default(),
-            terminal_defaults: defaults.terminal,
-            terminal_overrides: Default::default(),
-            language_defaults: defaults.languages,
-            language_overrides: Default::default(),
-            lsp: defaults.lsp.clone(),
-            theme: themes.get(&defaults.theme.unwrap()).unwrap(),
-            telemetry_defaults: defaults.telemetry,
-            telemetry_overrides: Default::default(),
-            auto_update: defaults.auto_update.unwrap(),
-            base_keymap: Default::default(),
-            features: Features {
-                copilot: defaults.features.copilot.unwrap(),
-            },
-            show_scrollbars: defaults.show_scrollbars.unwrap(),
-        }
-    }
-
-    // Fill out the overrride and etc. settings from the user's settings.json
-    pub fn set_user_settings(
-        &mut self,
-        data: SettingsFileContent,
-        theme_registry: &ThemeRegistry,
-        font_cache: &FontCache,
-    ) {
-        let mut family_changed = false;
-        if let Some(value) = data.buffer_font_family {
-            self.buffer_font_family_name = value;
-            family_changed = true;
-        }
-        if let Some(value) = data.buffer_font_features {
-            self.buffer_font_features = value;
-            family_changed = true;
-        }
-        if family_changed {
-            if let Some(id) = font_cache
-                .load_family(&[&self.buffer_font_family_name], &self.buffer_font_features)
-                .log_err()
-            {
-                self.buffer_font_family = id;
-            }
-        }
-
-        if let Some(value) = &data.theme {
-            if let Some(theme) = theme_registry.get(value).log_err() {
-                self.theme = theme;
-            }
-        }
-
-        merge(&mut self.buffer_font_size, data.buffer_font_size);
-        merge(
-            &mut self.active_pane_magnification,
-            data.active_pane_magnification,
-        );
-        merge(&mut self.default_buffer_font_size, data.buffer_font_size);
-        merge(&mut self.cursor_blink, data.cursor_blink);
-        merge(&mut self.confirm_quit, data.confirm_quit);
-        merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
-        merge(
-            &mut self.show_completions_on_input,
-            data.show_completions_on_input,
-        );
-        merge(&mut self.vim_mode, data.vim_mode);
-        merge(&mut self.autosave, data.autosave);
-        merge(&mut self.default_dock_anchor, data.default_dock_anchor);
-        merge(&mut self.base_keymap, data.base_keymap);
-        merge(&mut self.show_scrollbars, data.show_scrollbars);
-        merge(&mut self.features.copilot, data.features.copilot);
-
-        if let Some(copilot) = data.copilot {
-            if let Some(disabled_globs) = copilot.disabled_globs {
-                self.copilot.disabled_globs = disabled_globs
-                    .into_iter()
-                    .filter_map(|s| glob::Pattern::new(&s).ok())
-                    .collect()
-            }
-        }
-        self.editor_overrides = data.editor;
-        self.git_overrides = data.git.unwrap_or_default();
-        self.journal_overrides = data.journal;
-        self.terminal_defaults.font_size = data.terminal.font_size;
-        self.terminal_overrides.copy_on_select = data.terminal.copy_on_select;
-        self.terminal_overrides = data.terminal;
-        self.language_overrides = data.languages;
-        self.telemetry_overrides = data.telemetry;
-        self.lsp = data.lsp;
-        merge(&mut self.auto_update, data.auto_update);
-    }
-
-    pub fn with_language_defaults(
-        mut self,
-        language_name: impl Into<Arc<str>>,
-        overrides: EditorSettings,
-    ) -> Self {
-        self.language_defaults
-            .insert(language_name.into(), overrides);
-        self
-    }
-
-    pub fn features(&self) -> &Features {
-        &self.features
-    }
-
-    pub fn show_copilot_suggestions(&self, language: Option<&str>, path: Option<&Path>) -> bool {
-        if !self.features.copilot {
-            return false;
-        }
-
-        if !self.copilot_enabled_for_language(language) {
-            return false;
-        }
-
-        if let Some(path) = path {
-            if !self.copilot_enabled_for_path(path) {
-                return false;
-            }
-        }
-
-        true
-    }
-
-    pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
-        !self
-            .copilot
-            .disabled_globs
-            .iter()
-            .any(|glob| glob.matches_path(path))
-    }
-
-    pub fn copilot_enabled_for_language(&self, language: Option<&str>) -> bool {
-        self.language_setting(language, |settings| settings.show_copilot_suggestions)
-    }
-
-    pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
-        self.language_setting(language, |settings| settings.tab_size)
-    }
-
-    pub fn show_whitespaces(&self, language: Option<&str>) -> ShowWhitespaces {
-        self.language_setting(language, |settings| settings.show_whitespaces)
-    }
-
-    pub fn hard_tabs(&self, language: Option<&str>) -> bool {
-        self.language_setting(language, |settings| settings.hard_tabs)
-    }
-
-    pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
-        self.language_setting(language, |settings| settings.soft_wrap)
-    }
-
-    pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
-        self.language_setting(language, |settings| settings.preferred_line_length)
-    }
-
-    pub fn remove_trailing_whitespace_on_save(&self, language: Option<&str>) -> bool {
-        self.language_setting(language, |settings| {
-            settings.remove_trailing_whitespace_on_save.clone()
-        })
-    }
-
-    pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool {
-        self.language_setting(language, |settings| {
-            settings.ensure_final_newline_on_save.clone()
-        })
-    }
-
-    pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
-        self.language_setting(language, |settings| settings.format_on_save.clone())
-    }
-
-    pub fn formatter(&self, language: Option<&str>) -> Formatter {
-        self.language_setting(language, |settings| settings.formatter.clone())
-    }
-
-    pub fn enable_language_server(&self, language: Option<&str>) -> bool {
-        self.language_setting(language, |settings| settings.enable_language_server)
-    }
-
-    fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> R
-    where
-        F: Fn(&EditorSettings) -> Option<R>,
-    {
-        None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f)))
-            .or_else(|| f(&self.editor_overrides))
-            .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f)))
-            .or_else(|| f(&self.editor_defaults))
-            .expect("missing default")
-    }
-
-    pub fn git_gutter(&self) -> GitGutter {
-        self.git_overrides.git_gutter.unwrap_or_else(|| {
-            self.git
-                .git_gutter
-                .expect("git_gutter should be some by setting setup")
-        })
-    }
-
-    pub fn telemetry(&self) -> TelemetrySettings {
-        TelemetrySettings {
-            diagnostics: Some(self.telemetry_diagnostics()),
-            metrics: Some(self.telemetry_metrics()),
-        }
-    }
-
-    pub fn telemetry_diagnostics(&self) -> bool {
-        self.telemetry_overrides
-            .diagnostics
-            .or(self.telemetry_defaults.diagnostics)
-            .expect("missing default")
-    }
-
-    pub fn telemetry_metrics(&self) -> bool {
-        self.telemetry_overrides
-            .metrics
-            .or(self.telemetry_defaults.metrics)
-            .expect("missing default")
-    }
-
-    fn terminal_setting<F, R>(&self, f: F) -> R
-    where
-        F: Fn(&TerminalSettings) -> Option<R>,
-    {
-        None.or_else(|| f(&self.terminal_overrides))
-            .or_else(|| f(&self.terminal_defaults))
-            .expect("missing default")
-    }
-
-    pub fn terminal_line_height(&self) -> f32 {
-        self.terminal_setting(|terminal_setting| terminal_setting.line_height())
-    }
-
-    pub fn terminal_scroll(&self) -> AlternateScroll {
-        self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.to_owned())
-    }
-
-    pub fn terminal_shell(&self) -> Shell {
-        self.terminal_setting(|terminal_setting| terminal_setting.shell.to_owned())
-    }
-
-    pub fn terminal_env(&self) -> HashMap<String, String> {
-        self.terminal_setting(|terminal_setting| terminal_setting.env.to_owned())
-    }
-
-    pub fn terminal_strategy(&self) -> WorkingDirectory {
-        self.terminal_setting(|terminal_setting| terminal_setting.working_directory.to_owned())
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn test(cx: &gpui::AppContext) -> Settings {
-        Settings {
-            buffer_font_family_name: "Monaco".to_string(),
-            buffer_font_features: Default::default(),
-            buffer_font_family: cx
-                .font_cache()
-                .load_family(&["Monaco"], &Default::default())
-                .unwrap(),
-            buffer_font_size: 14.,
-            active_pane_magnification: 1.,
-            default_buffer_font_size: 14.,
-            confirm_quit: false,
-            cursor_blink: true,
-            hover_popover_enabled: true,
-            show_completions_on_input: true,
-            show_call_status_icon: true,
-            vim_mode: false,
-            autosave: Autosave::Off,
-            default_dock_anchor: DockAnchor::Bottom,
-            editor_defaults: EditorSettings {
-                tab_size: Some(4.try_into().unwrap()),
-                hard_tabs: Some(false),
-                soft_wrap: Some(SoftWrap::None),
-                preferred_line_length: Some(80),
-                remove_trailing_whitespace_on_save: Some(true),
-                ensure_final_newline_on_save: Some(true),
-                format_on_save: Some(FormatOnSave::On),
-                formatter: Some(Formatter::LanguageServer),
-                enable_language_server: Some(true),
-                show_copilot_suggestions: Some(true),
-                show_whitespaces: Some(ShowWhitespaces::None),
-            },
-            editor_overrides: Default::default(),
-            copilot: Default::default(),
-            journal_defaults: Default::default(),
-            journal_overrides: Default::default(),
-            terminal_defaults: Default::default(),
-            terminal_overrides: Default::default(),
-            git: Default::default(),
-            git_overrides: Default::default(),
-            language_defaults: Default::default(),
-            language_overrides: Default::default(),
-            lsp: Default::default(),
-            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
-            telemetry_defaults: TelemetrySettings {
-                diagnostics: Some(true),
-                metrics: Some(true),
-            },
-            telemetry_overrides: Default::default(),
-            auto_update: true,
-            base_keymap: Default::default(),
-            features: Features { copilot: true },
-            show_scrollbars: Default::default(),
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn test_async(cx: &mut gpui::TestAppContext) {
-        cx.update(|cx| {
-            let settings = Self::test(cx);
-            cx.set_global(settings);
-        });
-    }
-}
-
-pub fn settings_file_json_schema(
-    theme_names: Vec<String>,
-    language_names: &[String],
-) -> serde_json::Value {
-    let settings = SchemaSettings::draft07().with(|settings| {
-        settings.option_add_null_type = false;
-    });
-    let generator = SchemaGenerator::new(settings);
-
-    let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
-
-    // Create a schema for a theme name.
-    let theme_name_schema = SchemaObject {
-        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
-        enum_values: Some(theme_names.into_iter().map(Value::String).collect()),
-        ..Default::default()
-    };
-
-    // Create a schema for a 'languages overrides' object, associating editor
-    // settings with specific langauges.
-    assert!(root_schema.definitions.contains_key("EditorSettings"));
-
-    let languages_object_schema = SchemaObject {
-        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
-        object: Some(Box::new(ObjectValidation {
-            properties: language_names
-                .iter()
-                .map(|name| {
-                    (
-                        name.clone(),
-                        Schema::new_ref("#/definitions/EditorSettings".into()),
-                    )
-                })
-                .collect(),
-            ..Default::default()
-        })),
-        ..Default::default()
-    };
-
-    // Add these new schemas as definitions, and modify properties of the root
-    // schema to reference them.
-    root_schema.definitions.extend([
-        ("ThemeName".into(), theme_name_schema.into()),
-        ("Languages".into(), languages_object_schema.into()),
-    ]);
-    let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
-
-    root_schema_object.properties.extend([
-        (
-            "theme".to_owned(),
-            Schema::new_ref("#/definitions/ThemeName".into()),
-        ),
-        (
-            "languages".to_owned(),
-            Schema::new_ref("#/definitions/Languages".into()),
-        ),
-        // For backward compatibility
-        (
-            "language_overrides".to_owned(),
-            Schema::new_ref("#/definitions/Languages".into()),
-        ),
-    ]);
-
-    serde_json::to_value(root_schema).unwrap()
-}
-
-fn merge<T: Copy>(target: &mut T, value: Option<T>) {
-    if let Some(value) = value {
-        *target = value;
-    }
-}
-
-pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
-    Ok(serde_json::from_reader(
-        json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
-    )?)
-}
-
-lazy_static! {
-    static ref PAIR_QUERY: Query = Query::new(
-        tree_sitter_json::language(),
-        "
-            (pair
-                key: (string) @key
-                value: (_) @value)
-        ",
-    )
-    .unwrap();
-}
-
-fn update_object_in_settings_file<'a>(
-    old_object: &'a serde_json::Map<String, Value>,
-    new_object: &'a serde_json::Map<String, Value>,
-    text: &str,
-    syntax_tree: &Tree,
-    tab_size: usize,
-    key_path: &mut Vec<&'a str>,
-    edits: &mut Vec<(Range<usize>, String)>,
-) {
-    for (key, old_value) in old_object.iter() {
-        key_path.push(key);
-        let new_value = new_object.get(key).unwrap_or(&Value::Null);
-
-        // If the old and new values are both objects, then compare them key by key,
-        // preserving the comments and formatting of the unchanged parts. Otherwise,
-        // replace the old value with the new value.
-        if let (Value::Object(old_sub_object), Value::Object(new_sub_object)) =
-            (old_value, new_value)
-        {
-            update_object_in_settings_file(
-                old_sub_object,
-                new_sub_object,
-                text,
-                syntax_tree,
-                tab_size,
-                key_path,
-                edits,
-            )
-        } else if old_value != new_value {
-            let (range, replacement) =
-                update_key_in_settings_file(text, syntax_tree, &key_path, tab_size, &new_value);
-            edits.push((range, replacement));
-        }
-
-        key_path.pop();
-    }
-}
-
-fn update_key_in_settings_file(
-    text: &str,
-    syntax_tree: &Tree,
-    key_path: &[&str],
-    tab_size: usize,
-    new_value: impl Serialize,
-) -> (Range<usize>, String) {
-    const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
-    const LANGUAGES: &'static str = "languages";
-
-    let mut cursor = tree_sitter::QueryCursor::new();
-
-    let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
-
-    let mut depth = 0;
-    let mut last_value_range = 0..0;
-    let mut first_key_start = None;
-    let mut existing_value_range = 0..text.len();
-    let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
-    for mat in matches {
-        if mat.captures.len() != 2 {
-            continue;
-        }
-
-        let key_range = mat.captures[0].node.byte_range();
-        let value_range = mat.captures[1].node.byte_range();
-
-        // Don't enter sub objects until we find an exact
-        // match for the current keypath
-        if last_value_range.contains_inclusive(&value_range) {
-            continue;
-        }
-
-        last_value_range = value_range.clone();
-
-        if key_range.start > existing_value_range.end {
-            break;
-        }
-
-        first_key_start.get_or_insert_with(|| key_range.start);
-
-        let found_key = text
-            .get(key_range.clone())
-            .map(|key_text| {
-                if key_path[depth] == LANGUAGES && has_language_overrides {
-                    return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES);
-                } else {
-                    return key_text == format!("\"{}\"", key_path[depth]);
-                }
-            })
-            .unwrap_or(false);
-
-        if found_key {
-            existing_value_range = value_range;
-            // Reset last value range when increasing in depth
-            last_value_range = existing_value_range.start..existing_value_range.start;
-            depth += 1;
-
-            if depth == key_path.len() {
-                break;
-            } else {
-                first_key_start = None;
-            }
-        }
-    }
-
-    // We found the exact key we want, insert the new value
-    if depth == key_path.len() {
-        let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth);
-        (existing_value_range, new_val)
-    } else {
-        // We have key paths, construct the sub objects
-        let new_key = if has_language_overrides && key_path[depth] == LANGUAGES {
-            LANGUAGE_OVERRIDES
-        } else {
-            key_path[depth]
-        };
-
-        // We don't have the key, construct the nested objects
-        let mut new_value = serde_json::to_value(new_value).unwrap();
-        for key in key_path[(depth + 1)..].iter().rev() {
-            if has_language_overrides && key == &LANGUAGES {
-                new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value });
-            } else {
-                new_value = serde_json::json!({ key.to_string(): new_value });
-            }
-        }
-
-        if let Some(first_key_start) = first_key_start {
-            let mut row = 0;
-            let mut column = 0;
-            for (ix, char) in text.char_indices() {
-                if ix == first_key_start {
-                    break;
-                }
-                if char == '\n' {
-                    row += 1;
-                    column = 0;
-                } else {
-                    column += char.len_utf8();
-                }
-            }
-
-            if row > 0 {
-                // depth is 0 based, but division needs to be 1 based.
-                let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
-                let space = ' ';
-                let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
-                (first_key_start..first_key_start, content)
-            } else {
-                let new_val = serde_json::to_string(&new_value).unwrap();
-                let mut content = format!(r#""{new_key}": {new_val},"#);
-                content.push(' ');
-                (first_key_start..first_key_start, content)
-            }
-        } else {
-            new_value = serde_json::json!({ new_key.to_string(): new_value });
-            let indent_prefix_len = 4 * depth;
-            let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
-            if depth == 0 {
-                new_val.push('\n');
-            }
-
-            (existing_value_range, new_val)
-        }
-    }
-}
-
-fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String {
-    const SPACES: [u8; 32] = [b' '; 32];
-
-    debug_assert!(indent_size <= SPACES.len());
-    debug_assert!(indent_prefix_len <= SPACES.len());
-
-    let mut output = Vec::new();
-    let mut ser = serde_json::Serializer::with_formatter(
-        &mut output,
-        serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
-    );
-
-    value.serialize(&mut ser).unwrap();
-    let text = String::from_utf8(output).unwrap();
-
-    let mut adjusted_text = String::new();
-    for (i, line) in text.split('\n').enumerate() {
-        if i > 0 {
-            adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
-        }
-        adjusted_text.push_str(line);
-        adjusted_text.push('\n');
-    }
-    adjusted_text.pop();
-    adjusted_text
-}
-
-/// Update the settings file with the given callback.
-///
-/// Returns a new JSON string and the offset where the first edit occurred.
-fn update_settings_file(
-    text: &str,
-    mut old_file_content: SettingsFileContent,
-    tab_size: NonZeroU32,
-    update: impl FnOnce(&mut SettingsFileContent),
-) -> Vec<(Range<usize>, String)> {
-    let mut new_file_content = old_file_content.clone();
-    update(&mut new_file_content);
-
-    if new_file_content.languages.len() != old_file_content.languages.len() {
-        for language in new_file_content.languages.keys() {
-            old_file_content
-                .languages
-                .entry(language.clone())
-                .or_default();
-        }
-        for language in old_file_content.languages.keys() {
-            new_file_content
-                .languages
-                .entry(language.clone())
-                .or_default();
-        }
-    }
-
-    let mut parser = tree_sitter::Parser::new();
-    parser.set_language(tree_sitter_json::language()).unwrap();
-    let tree = parser.parse(text, None).unwrap();
-
-    let old_object = to_json_object(old_file_content);
-    let new_object = to_json_object(new_file_content);
-    let mut key_path = Vec::new();
-    let mut edits = Vec::new();
-    update_object_in_settings_file(
-        &old_object,
-        &new_object,
-        &text,
-        &tree,
-        tab_size.get() as usize,
-        &mut key_path,
-        &mut edits,
-    );
-    edits.sort_unstable_by_key(|e| e.0.start);
-    return edits;
-}
-
-fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map<String, Value> {
-    let tmp = serde_json::to_value(settings_file).unwrap();
-    match tmp {
-        Value::Object(map) => map,
-        _ => unreachable!("SettingsFileContent represents a JSON map"),
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use unindent::Unindent;
-
-    fn assert_new_settings(
-        old_json: String,
-        update: fn(&mut SettingsFileContent),
-        expected_new_json: String,
-    ) {
-        let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
-        let edits = update_settings_file(&old_json, old_content, 4.try_into().unwrap(), update);
-        let mut new_json = old_json;
-        for (range, replacement) in edits.into_iter().rev() {
-            new_json.replace_range(range, &replacement);
-        }
-        pretty_assertions::assert_eq!(new_json, expected_new_json);
-    }
-
-    #[test]
-    fn test_update_language_overrides_copilot() {
-        assert_new_settings(
-            r#"
-                {
-                    "language_overrides": {
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings.languages.insert(
-                    "Rust".into(),
-                    EditorSettings {
-                        show_copilot_suggestions: Some(true),
-                        ..Default::default()
-                    },
-                );
-            },
-            r#"
-                {
-                    "language_overrides": {
-                        "Rust": {
-                            "show_copilot_suggestions": true
-                        },
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_copilot_globs() {
-        assert_new_settings(
-            r#"
-                {
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings.copilot = Some(CopilotSettingsContent {
-                    disabled_globs: Some(vec![]),
-                });
-            },
-            r#"
-                {
-                    "copilot": {
-                        "disabled_globs": []
-                    }
-                }
-            "#
-            .unindent(),
-        );
-
-        assert_new_settings(
-            r#"
-                {
-                    "copilot": {
-                        "disabled_globs": [
-                            "**/*.json"
-                        ]
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings
-                    .copilot
-                    .get_or_insert(Default::default())
-                    .disabled_globs
-                    .as_mut()
-                    .unwrap()
-                    .push(".env".into());
-            },
-            r#"
-                {
-                    "copilot": {
-                        "disabled_globs": [
-                            "**/*.json",
-                            ".env"
-                        ]
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_copilot() {
-        assert_new_settings(
-            r#"
-                {
-                    "languages": {
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings.editor.show_copilot_suggestions = Some(true);
-            },
-            r#"
-                {
-                    "show_copilot_suggestions": true,
-                    "languages": {
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_language_copilot() {
-        assert_new_settings(
-            r#"
-                {
-                    "languages": {
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings.languages.insert(
-                    "Rust".into(),
-                    EditorSettings {
-                        show_copilot_suggestions: Some(true),
-                        ..Default::default()
-                    },
-                );
-            },
-            r#"
-                {
-                    "languages": {
-                        "Rust": {
-                            "show_copilot_suggestions": true
-                        },
-                        "JSON": {
-                            "show_copilot_suggestions": false
-                        }
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting_multiple_fields() {
-        assert_new_settings(
-            r#"
-                {
-                    "telemetry": {
-                        "metrics": false,
-                        "diagnostics": false
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| {
-                settings.telemetry.set_diagnostics(true);
-                settings.telemetry.set_metrics(true);
-            },
-            r#"
-                {
-                    "telemetry": {
-                        "metrics": true,
-                        "diagnostics": true
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting_weird_formatting() {
-        assert_new_settings(
-            r#"{
-                "telemetry":   { "metrics": false, "diagnostics": true }
-            }"#
-            .unindent(),
-            |settings| settings.telemetry.set_diagnostics(false),
-            r#"{
-                "telemetry":   { "metrics": false, "diagnostics": false }
-            }"#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting_other_fields() {
-        assert_new_settings(
-            r#"
-                {
-                    "telemetry": {
-                        "metrics": false,
-                        "diagnostics": true
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| settings.telemetry.set_diagnostics(false),
-            r#"
-                {
-                    "telemetry": {
-                        "metrics": false,
-                        "diagnostics": false
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting_empty_telemetry() {
-        assert_new_settings(
-            r#"
-                {
-                    "telemetry": {}
-                }
-            "#
-            .unindent(),
-            |settings| settings.telemetry.set_diagnostics(false),
-            r#"
-                {
-                    "telemetry": {
-                        "diagnostics": false
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting_pre_existing() {
-        assert_new_settings(
-            r#"
-                {
-                    "telemetry": {
-                        "diagnostics": true
-                    }
-                }
-            "#
-            .unindent(),
-            |settings| settings.telemetry.set_diagnostics(false),
-            r#"
-                {
-                    "telemetry": {
-                        "diagnostics": false
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_telemetry_setting() {
-        assert_new_settings(
-            "{}".into(),
-            |settings| settings.telemetry.set_diagnostics(true),
-            r#"
-                {
-                    "telemetry": {
-                        "diagnostics": true
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_update_object_empty_doc() {
-        assert_new_settings(
-            "".into(),
-            |settings| settings.telemetry.set_diagnostics(true),
-            r#"
-                {
-                    "telemetry": {
-                        "diagnostics": true
-                    }
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_write_theme_into_settings_with_theme() {
-        assert_new_settings(
-            r#"
-                {
-                    "theme": "One Dark"
-                }
-            "#
-            .unindent(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"
-                {
-                    "theme": "summerfruit-light"
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_write_theme_into_empty_settings() {
-        assert_new_settings(
-            r#"
-                {
-                }
-            "#
-            .unindent(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"
-                {
-                    "theme": "summerfruit-light"
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn write_key_no_document() {
-        assert_new_settings(
-            "".to_string(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"
-                {
-                    "theme": "summerfruit-light"
-                }
-            "#
-            .unindent(),
-        );
-    }
-
-    #[test]
-    fn test_write_theme_into_single_line_settings_without_theme() {
-        assert_new_settings(
-            r#"{ "a": "", "ok": true }"#.to_string(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#.to_string(),
-        );
-    }
-
-    #[test]
-    fn test_write_theme_pre_object_whitespace() {
-        assert_new_settings(
-            r#"          { "a": "", "ok": true }"#.to_string(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"          { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
-        );
-    }
-
-    #[test]
-    fn test_write_theme_into_multi_line_settings_without_theme() {
-        assert_new_settings(
-            r#"
-                {
-                    "a": "b"
-                }
-            "#
-            .unindent(),
-            |settings| settings.theme = Some("summerfruit-light".to_string()),
-            r#"
-                {
-                    "theme": "summerfruit-light",
-                    "a": "b"
-                }
-            "#
-            .unindent(),
-        );
+pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
+    match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
+        Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+        Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
     }
 }

crates/settings/src/settings_file.rs 🔗

@@ -1,367 +1,138 @@
-use crate::{update_settings_file, watched_json::WatchedJsonFile, Settings, SettingsFileContent};
+use crate::{settings_store::SettingsStore, Setting, DEFAULT_SETTINGS_ASSET_PATH};
 use anyhow::Result;
 use assets::Assets;
 use fs::Fs;
-use gpui::AppContext;
-use std::{io::ErrorKind, ops::Range, path::Path, sync::Arc};
-
-// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
-//       And instant updates in the Zed editor
-#[derive(Clone)]
-pub struct SettingsFile {
-    path: &'static Path,
-    settings_file_content: WatchedJsonFile<SettingsFileContent>,
-    fs: Arc<dyn Fs>,
+use futures::{channel::mpsc, StreamExt};
+use gpui::{executor::Background, AppContext, AssetSource};
+use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
+use util::{paths, ResultExt};
+
+pub fn register<T: Setting>(cx: &mut AppContext) {
+    cx.update_global::<SettingsStore, _, _>(|store, cx| {
+        store.register_setting::<T>(cx);
+    });
 }
 
-impl SettingsFile {
-    pub fn new(
-        path: &'static Path,
-        settings_file_content: WatchedJsonFile<SettingsFileContent>,
-        fs: Arc<dyn Fs>,
-    ) -> Self {
-        SettingsFile {
-            path,
-            settings_file_content,
-            fs,
-        }
-    }
-
-    async fn load_settings(path: &Path, fs: &Arc<dyn Fs>) -> Result<String> {
-        match fs.load(path).await {
-            result @ Ok(_) => result,
-            Err(err) => {
-                if let Some(e) = err.downcast_ref::<std::io::Error>() {
-                    if e.kind() == ErrorKind::NotFound {
-                        return Ok(Settings::initial_user_settings_content(&Assets).to_string());
-                    }
-                }
-                return Err(err);
-            }
-        }
-    }
-
-    pub fn update_unsaved(
-        text: &str,
-        cx: &AppContext,
-        update: impl FnOnce(&mut SettingsFileContent),
-    ) -> Vec<(Range<usize>, String)> {
-        let this = cx.global::<SettingsFile>();
-        let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
-        let current_file_content = this.settings_file_content.current();
-        update_settings_file(&text, current_file_content, tab_size, update)
-    }
-
-    pub fn update(
-        cx: &mut AppContext,
-        update: impl 'static + Send + FnOnce(&mut SettingsFileContent),
-    ) {
-        let this = cx.global::<SettingsFile>();
-        let tab_size = cx.global::<Settings>().tab_size(Some("JSON"));
-        let current_file_content = this.settings_file_content.current();
-        let fs = this.fs.clone();
-        let path = this.path.clone();
-
-        cx.background()
-            .spawn(async move {
-                let old_text = SettingsFile::load_settings(path, &fs).await?;
-                let edits = update_settings_file(&old_text, current_file_content, tab_size, update);
-                let mut new_text = old_text;
-                for (range, replacement) in edits.into_iter().rev() {
-                    new_text.replace_range(range, &replacement);
-                }
-                fs.atomic_write(path.to_path_buf(), new_text).await?;
-                anyhow::Ok(())
-            })
-            .detach_and_log_err(cx)
-    }
+pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T {
+    cx.global::<SettingsStore>().get(None)
 }
 
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::{
-        watch_files, watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap,
-    };
-    use fs::FakeFs;
-    use gpui::{actions, elements::*, Action, Entity, TestAppContext, View, ViewContext};
-    use theme::ThemeRegistry;
-
-    struct TestView;
-
-    impl Entity for TestView {
-        type Event = ();
+pub fn default_settings() -> Cow<'static, str> {
+    match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
+        Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+        Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
     }
+}
 
-    impl View for TestView {
-        fn ui_name() -> &'static str {
-            "TestView"
-        }
-
-        fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
-            Empty::new().into_any()
-        }
-    }
-
-    #[gpui::test]
-    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
-        let executor = cx.background();
-        let fs = FakeFs::new(executor.clone());
-        let font_cache = cx.font_cache();
-
-        actions!(test, [A, B]);
-        // From the Atom keymap
-        actions!(workspace, [ActivatePreviousPane]);
-        // From the JetBrains keymap
-        actions!(pane, [ActivatePrevItem]);
-
-        fs.save(
-            "/settings.json".as_ref(),
-            &r#"
-            {
-                "base_keymap": "Atom"
-            }
-            "#
-            .into(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
+#[cfg(any(test, feature = "test-support"))]
+pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
+
+#[cfg(any(test, feature = "test-support"))]
+pub fn test_settings() -> String {
+    let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>(
+        default_settings().as_ref(),
+    )
+    .unwrap();
+    util::merge_non_null_json_value_into(
+        serde_json::json!({
+            "buffer_font_family": "Courier",
+            "buffer_font_features": {},
+            "buffer_font_size": 14,
+            "theme": EMPTY_THEME_NAME,
+        }),
+        &mut value,
+    );
+    value.as_object_mut().unwrap().remove("languages");
+    serde_json::to_string(&value).unwrap()
+}
 
-        fs.save(
-            "/keymap.json".as_ref(),
-            &r#"
-            [
-                {
-                    "bindings": {
-                        "backspace": "test::A"
+pub fn watch_config_file(
+    executor: Arc<Background>,
+    fs: Arc<dyn Fs>,
+    path: PathBuf,
+) -> mpsc::UnboundedReceiver<String> {
+    let (tx, rx) = mpsc::unbounded();
+    executor
+        .spawn(async move {
+            let events = fs.watch(&path, Duration::from_millis(100)).await;
+            futures::pin_mut!(events);
+            loop {
+                if let Ok(contents) = fs.load(&path).await {
+                    if !tx.unbounded_send(contents).is_ok() {
+                        break;
                     }
                 }
-            ]
-            "#
-            .into(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
-
-        let settings_file =
-            WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
-        let keymaps_file =
-            WatchedJsonFile::new(fs.clone(), &executor, "/keymap.json".as_ref()).await;
-
-        let default_settings = cx.read(Settings::test);
-
-        cx.update(|cx| {
-            cx.add_global_action(|_: &A, _cx| {});
-            cx.add_global_action(|_: &B, _cx| {});
-            cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
-            cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
-            watch_files(
-                default_settings,
-                settings_file,
-                ThemeRegistry::new((), font_cache),
-                keymaps_file,
-                cx,
-            )
-        });
-
-        cx.foreground().run_until_parked();
-
-        let (window_id, _view) = cx.add_window(|_| TestView);
-
-        // Test loading the keymap base at all
-        assert_key_bindings_for(
-            window_id,
-            cx,
-            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
-            line!(),
-        );
-
-        // Test modifying the users keymap, while retaining the base keymap
-        fs.save(
-            "/keymap.json".as_ref(),
-            &r#"
-            [
-                {
-                    "bindings": {
-                        "backspace": "test::B"
-                    }
+                if events.next().await.is_none() {
+                    break;
                 }
-            ]
-            "#
-            .into(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
-
-        cx.foreground().run_until_parked();
-
-        assert_key_bindings_for(
-            window_id,
-            cx,
-            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
-            line!(),
-        );
-
-        // Test modifying the base, while retaining the users keymap
-        fs.save(
-            "/settings.json".as_ref(),
-            &r#"
-            {
-                "base_keymap": "JetBrains"
             }
-            "#
-            .into(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
-
-        cx.foreground().run_until_parked();
-
-        assert_key_bindings_for(
-            window_id,
-            cx,
-            vec![("backspace", &B), ("[", &ActivatePrevItem)],
-            line!(),
-        );
-    }
+        })
+        .detach();
+    rx
+}
 
-    fn assert_key_bindings_for<'a>(
-        window_id: usize,
-        cx: &TestAppContext,
-        actions: Vec<(&'static str, &'a dyn Action)>,
-        line: u32,
-    ) {
-        for (key, action) in actions {
-            // assert that...
-            assert!(
-                cx.available_actions(window_id, 0)
-                    .into_iter()
-                    .any(|(_, bound_action, b)| {
-                        // action names match...
-                        bound_action.name() == action.name()
-                    && bound_action.namespace() == action.namespace()
-                    // and key strokes contain the given key
-                    && b.iter()
-                        .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
-                    }),
-                "On {} Failed to find {} with key binding {}",
-                line,
-                action.name(),
-                key
-            );
+pub fn handle_settings_file_changes(
+    mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
+    cx: &mut AppContext,
+) {
+    let user_settings_content = cx.background().block(user_settings_file_rx.next()).unwrap();
+    cx.update_global::<SettingsStore, _, _>(|store, cx| {
+        store
+            .set_user_settings(&user_settings_content, cx)
+            .log_err();
+    });
+    cx.spawn(move |mut cx| async move {
+        while let Some(user_settings_content) = user_settings_file_rx.next().await {
+            cx.update(|cx| {
+                cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                    store
+                        .set_user_settings(&user_settings_content, cx)
+                        .log_err();
+                });
+                cx.refresh_windows();
+            });
         }
-    }
-
-    #[gpui::test]
-    async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
-        let executor = cx.background();
-        let fs = FakeFs::new(executor.clone());
-        let font_cache = cx.font_cache();
+    })
+    .detach();
+}
 
-        fs.save(
-            "/settings.json".as_ref(),
-            &r#"
-            {
-                "buffer_font_size": 24,
-                "soft_wrap": "editor_width",
-                "tab_size": 8,
-                "language_overrides": {
-                    "Markdown": {
-                        "tab_size": 2,
-                        "preferred_line_length": 100,
-                        "soft_wrap": "preferred_line_length"
-                    }
+async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
+    match fs.load(&paths::SETTINGS).await {
+        result @ Ok(_) => result,
+        Err(err) => {
+            if let Some(e) = err.downcast_ref::<std::io::Error>() {
+                if e.kind() == ErrorKind::NotFound {
+                    return Ok(crate::initial_user_settings_content(&Assets).to_string());
                 }
             }
-            "#
-            .into(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
+            return Err(err);
+        }
+    }
+}
 
-        let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
+pub fn update_settings_file<T: Setting>(
+    fs: Arc<dyn Fs>,
+    cx: &mut AppContext,
+    update: impl 'static + Send + FnOnce(&mut T::FileContent),
+) {
+    cx.spawn(|cx| async move {
+        let old_text = cx
+            .background()
+            .spawn({
+                let fs = fs.clone();
+                async move { load_settings(&fs).await }
+            })
+            .await?;
 
-        let default_settings = cx.read(Settings::test).with_language_defaults(
-            "JavaScript",
-            EditorSettings {
-                tab_size: Some(2.try_into().unwrap()),
-                ..Default::default()
-            },
-        );
-        cx.update(|cx| {
-            watch_settings_file(
-                default_settings.clone(),
-                source,
-                ThemeRegistry::new((), font_cache),
-                cx,
-            )
+        let new_text = cx.read(|cx| {
+            cx.global::<SettingsStore>()
+                .new_text_for_update::<T>(old_text, update)
         });
 
-        cx.foreground().run_until_parked();
-        let settings = cx.read(|cx| cx.global::<Settings>().clone());
-        assert_eq!(settings.buffer_font_size, 24.0);
-
-        assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
-        assert_eq!(
-            settings.soft_wrap(Some("Markdown")),
-            SoftWrap::PreferredLineLength
-        );
-        assert_eq!(
-            settings.soft_wrap(Some("JavaScript")),
-            SoftWrap::EditorWidth
-        );
-
-        assert_eq!(settings.preferred_line_length(None), 80);
-        assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
-        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
-
-        assert_eq!(settings.tab_size(None).get(), 8);
-        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
-        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
-
-        fs.save(
-            "/settings.json".as_ref(),
-            &"(garbage)".into(),
-            Default::default(),
-        )
-        .await
-        .unwrap();
-        // fs.remove_file("/settings.json".as_ref(), Default::default())
-        //     .await
-        //     .unwrap();
-
-        cx.foreground().run_until_parked();
-        let settings = cx.read(|cx| cx.global::<Settings>().clone());
-        assert_eq!(settings.buffer_font_size, 24.0);
-
-        assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
-        assert_eq!(
-            settings.soft_wrap(Some("Markdown")),
-            SoftWrap::PreferredLineLength
-        );
-        assert_eq!(
-            settings.soft_wrap(Some("JavaScript")),
-            SoftWrap::EditorWidth
-        );
-
-        assert_eq!(settings.preferred_line_length(None), 80);
-        assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
-        assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
-
-        assert_eq!(settings.tab_size(None).get(), 8);
-        assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
-        assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
-
-        fs.remove_file("/settings.json".as_ref(), Default::default())
-            .await
-            .unwrap();
-        cx.foreground().run_until_parked();
-        let settings = cx.read(|cx| cx.global::<Settings>().clone());
-        assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
-    }
+        cx.background()
+            .spawn(async move { fs.atomic_write(paths::SETTINGS.clone(), new_text).await })
+            .await?;
+        anyhow::Ok(())
+    })
+    .detach_and_log_err(cx);
 }

crates/settings/src/settings_store.rs 🔗

@@ -0,0 +1,1246 @@
+use anyhow::Result;
+use collections::{btree_map, hash_map, BTreeMap, HashMap};
+use gpui::AppContext;
+use lazy_static::lazy_static;
+use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
+use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
+use smallvec::SmallVec;
+use std::{
+    any::{type_name, Any, TypeId},
+    fmt::Debug,
+    ops::Range,
+    path::Path,
+    str,
+    sync::Arc,
+};
+use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
+
+/// A value that can be defined as a user setting.
+///
+/// Settings can be loaded from a combination of multiple JSON files.
+pub trait Setting: 'static {
+    /// The name of a key within the JSON file from which this setting should
+    /// be deserialized. If this is `None`, then the setting will be deserialized
+    /// from the root object.
+    const KEY: Option<&'static str>;
+
+    /// The type that is stored in an individual JSON file.
+    type FileContent: Clone + Serialize + DeserializeOwned + JsonSchema;
+
+    /// The logic for combining together values from one or more JSON files into the
+    /// final value for this setting.
+    ///
+    /// The user values are ordered from least specific (the global settings file)
+    /// to most specific (the innermost local settings file).
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        cx: &AppContext,
+    ) -> Result<Self>
+    where
+        Self: Sized;
+
+    fn json_schema(
+        generator: &mut SchemaGenerator,
+        _: &SettingsJsonSchemaParams,
+        _: &AppContext,
+    ) -> RootSchema {
+        generator.root_schema_for::<Self::FileContent>()
+    }
+
+    fn json_merge(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+    ) -> Result<Self::FileContent> {
+        let mut merged = serde_json::Value::Null;
+        for value in [default_value].iter().chain(user_values) {
+            merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
+        }
+        Ok(serde_json::from_value(merged)?)
+    }
+
+    fn load_via_json_merge(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+    ) -> Result<Self>
+    where
+        Self: DeserializeOwned,
+    {
+        let mut merged = serde_json::Value::Null;
+        for value in [default_value].iter().chain(user_values) {
+            merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
+        }
+        Ok(serde_json::from_value(merged)?)
+    }
+
+    fn missing_default() -> anyhow::Error {
+        anyhow::anyhow!("missing default")
+    }
+}
+
+pub struct SettingsJsonSchemaParams<'a> {
+    pub staff_mode: bool,
+    pub language_names: &'a [String],
+}
+
+/// A set of strongly-typed setting values defined via multiple JSON files.
+#[derive(Default)]
+pub struct SettingsStore {
+    setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
+    default_deserialized_settings: Option<serde_json::Value>,
+    user_deserialized_settings: Option<serde_json::Value>,
+    local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
+    tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
+}
+
+#[derive(Debug)]
+struct SettingValue<T> {
+    global_value: Option<T>,
+    local_values: Vec<(Arc<Path>, T)>,
+}
+
+trait AnySettingValue {
+    fn key(&self) -> Option<&'static str>;
+    fn setting_type_name(&self) -> &'static str;
+    fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting>;
+    fn load_setting(
+        &self,
+        default_value: &DeserializedSetting,
+        custom: &[DeserializedSetting],
+        cx: &AppContext,
+    ) -> Result<Box<dyn Any>>;
+    fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
+    fn set_global_value(&mut self, value: Box<dyn Any>);
+    fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>);
+    fn json_schema(
+        &self,
+        generator: &mut SchemaGenerator,
+        _: &SettingsJsonSchemaParams,
+        cx: &AppContext,
+    ) -> RootSchema;
+}
+
+struct DeserializedSetting(Box<dyn Any>);
+
+impl SettingsStore {
+    /// Add a new type of setting to the store.
+    pub fn register_setting<T: Setting>(&mut self, cx: &AppContext) {
+        let setting_type_id = TypeId::of::<T>();
+        let entry = self.setting_values.entry(setting_type_id);
+        if matches!(entry, hash_map::Entry::Occupied(_)) {
+            return;
+        }
+
+        let setting_value = entry.or_insert(Box::new(SettingValue::<T> {
+            global_value: None,
+            local_values: Vec::new(),
+        }));
+
+        if let Some(default_settings) = &self.default_deserialized_settings {
+            if let Some(default_settings) = setting_value
+                .deserialize_setting(default_settings)
+                .log_err()
+            {
+                let mut user_values_stack = Vec::new();
+
+                if let Some(user_settings) = &self.user_deserialized_settings {
+                    if let Some(user_settings) =
+                        setting_value.deserialize_setting(user_settings).log_err()
+                    {
+                        user_values_stack = vec![user_settings];
+                    }
+                }
+
+                if let Some(setting) = setting_value
+                    .load_setting(&default_settings, &user_values_stack, cx)
+                    .log_err()
+                {
+                    setting_value.set_global_value(setting);
+                }
+            }
+        }
+    }
+
+    /// Get the value of a setting.
+    ///
+    /// Panics if the given setting type has not been registered, or if there is no
+    /// value for this setting.
+    pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T {
+        self.setting_values
+            .get(&TypeId::of::<T>())
+            .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
+            .value_for_path(path)
+            .downcast_ref::<T>()
+            .expect("no default value for setting type")
+    }
+
+    /// Override the global value for a setting.
+    ///
+    /// The given value will be overwritten if the user settings file changes.
+    pub fn override_global<T: Setting>(&mut self, value: T) {
+        self.setting_values
+            .get_mut(&TypeId::of::<T>())
+            .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
+            .set_global_value(Box::new(value))
+    }
+
+    /// Get the user's settings as a raw JSON value.
+    ///
+    /// This is only for debugging and reporting. For user-facing functionality,
+    /// use the typed setting interface.
+    pub fn untyped_user_settings(&self) -> &serde_json::Value {
+        self.user_deserialized_settings
+            .as_ref()
+            .unwrap_or(&serde_json::Value::Null)
+    }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test(cx: &AppContext) -> Self {
+        let mut this = Self::default();
+        this.set_default_settings(&crate::test_settings(), cx)
+            .unwrap();
+        this.set_user_settings("{}", cx).unwrap();
+        this
+    }
+
+    /// Update the value of a setting in the user's global configuration.
+    ///
+    /// 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: Setting>(
+        &mut self,
+        cx: &AppContext,
+        update: impl FnOnce(&mut T::FileContent),
+    ) {
+        if self.user_deserialized_settings.is_none() {
+            self.set_user_settings("{}", cx).unwrap();
+        }
+        let old_text =
+            serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap();
+        let new_text = self.new_text_for_update::<T>(old_text, update);
+        self.set_user_settings(&new_text, cx).unwrap();
+    }
+
+    /// Update the value of a setting in a JSON file, returning the new text
+    /// for that JSON file.
+    pub fn new_text_for_update<T: Setting>(
+        &self,
+        old_text: String,
+        update: impl FnOnce(&mut T::FileContent),
+    ) -> String {
+        let edits = self.edits_for_update::<T>(&old_text, update);
+        let mut new_text = old_text;
+        for (range, replacement) in edits.into_iter() {
+            new_text.replace_range(range, &replacement);
+        }
+        new_text
+    }
+
+    /// Update 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: Setting>(
+        &self,
+        text: &str,
+        update: impl FnOnce(&mut T::FileContent),
+    ) -> Vec<(Range<usize>, String)> {
+        let setting_type_id = TypeId::of::<T>();
+
+        let old_content = self
+            .setting_values
+            .get(&setting_type_id)
+            .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
+            .deserialize_setting(
+                self.user_deserialized_settings
+                    .as_ref()
+                    .expect("no user settings loaded"),
+            )
+            .unwrap_or_else(|e| {
+                panic!(
+                    "could not deserialize setting type {} from user settings: {}",
+                    type_name::<T>(),
+                    e
+                )
+            })
+            .0
+            .downcast::<T::FileContent>()
+            .unwrap();
+        let mut new_content = old_content.clone();
+        update(&mut new_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) = T::KEY {
+            key_path.push(key);
+        }
+
+        let mut edits = Vec::new();
+        let tab_size = self.json_tab_size();
+        let mut text = text.to_string();
+        update_value_in_json_text(
+            &mut text,
+            &mut key_path,
+            tab_size,
+            &old_value,
+            &new_value,
+            &mut edits,
+        );
+        return edits;
+    }
+
+    /// Configure the tab sized when updating JSON files.
+    pub fn set_json_tab_size_callback<T: Setting>(
+        &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())),
+        ));
+    }
+
+    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
+    }
+
+    /// Set the default settings via a JSON string.
+    ///
+    /// The string should contain a JSON object with a default value for every setting.
+    pub fn set_default_settings(
+        &mut self,
+        default_settings_content: &str,
+        cx: &AppContext,
+    ) -> Result<()> {
+        self.default_deserialized_settings =
+            Some(parse_json_with_comments(default_settings_content)?);
+        self.recompute_values(None, cx)?;
+        Ok(())
+    }
+
+    /// Set the user settings via a JSON string.
+    pub fn set_user_settings(
+        &mut self,
+        user_settings_content: &str,
+        cx: &AppContext,
+    ) -> Result<()> {
+        self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?);
+        self.recompute_values(None, cx)?;
+        Ok(())
+    }
+
+    /// Add or remove a set of local settings via a JSON string.
+    pub fn set_local_settings(
+        &mut self,
+        path: Arc<Path>,
+        settings_content: Option<&str>,
+        cx: &AppContext,
+    ) -> Result<()> {
+        if let Some(content) = settings_content {
+            self.local_deserialized_settings
+                .insert(path.clone(), parse_json_with_comments(content)?);
+        } else {
+            self.local_deserialized_settings.remove(&path);
+        }
+        self.recompute_values(Some(&path), cx)?;
+        Ok(())
+    }
+
+    pub fn json_schema(
+        &self,
+        schema_params: &SettingsJsonSchemaParams,
+        cx: &AppContext,
+    ) -> serde_json::Value {
+        use schemars::{
+            gen::SchemaSettings,
+            schema::{Schema, SchemaObject},
+        };
+
+        let settings = SchemaSettings::draft07().with(|settings| {
+            settings.option_add_null_type = false;
+        });
+        let mut generator = SchemaGenerator::new(settings);
+        let mut combined_schema = RootSchema::default();
+
+        for setting_value in self.setting_values.values() {
+            let setting_schema = setting_value.json_schema(&mut generator, schema_params, cx);
+            combined_schema
+                .definitions
+                .extend(setting_schema.definitions);
+
+            let target_schema = if let Some(key) = setting_value.key() {
+                let key_schema = combined_schema
+                    .schema
+                    .object()
+                    .properties
+                    .entry(key.to_string())
+                    .or_insert_with(|| Schema::Object(SchemaObject::default()));
+                if let Schema::Object(key_schema) = key_schema {
+                    key_schema
+                } else {
+                    continue;
+                }
+            } else {
+                &mut combined_schema.schema
+            };
+
+            merge_schema(target_schema, setting_schema.schema);
+        }
+
+        fn merge_schema(target: &mut SchemaObject, source: SchemaObject) {
+            if let Some(source) = source.object {
+                let target_properties = &mut target.object().properties;
+                for (key, value) in source.properties {
+                    match target_properties.entry(key) {
+                        btree_map::Entry::Vacant(e) => {
+                            e.insert(value);
+                        }
+                        btree_map::Entry::Occupied(e) => {
+                            if let (Schema::Object(target), Schema::Object(src)) =
+                                (e.into_mut(), value)
+                            {
+                                merge_schema(target, src);
+                            }
+                        }
+                    }
+                }
+            }
+
+            overwrite(&mut target.instance_type, source.instance_type);
+            overwrite(&mut target.string, source.string);
+            overwrite(&mut target.number, source.number);
+            overwrite(&mut target.reference, source.reference);
+            overwrite(&mut target.array, source.array);
+            overwrite(&mut target.enum_values, source.enum_values);
+
+            fn overwrite<T>(target: &mut Option<T>, source: Option<T>) {
+                if let Some(source) = source {
+                    *target = Some(source);
+                }
+            }
+        }
+
+        serde_json::to_value(&combined_schema).unwrap()
+    }
+
+    fn recompute_values(
+        &mut self,
+        changed_local_path: Option<&Path>,
+        cx: &AppContext,
+    ) -> Result<()> {
+        // Reload the global and local values for every setting.
+        let mut user_settings_stack = Vec::<DeserializedSetting>::new();
+        let mut paths_stack = Vec::<Option<&Path>>::new();
+        for setting_value in self.setting_values.values_mut() {
+            if let Some(default_settings) = &self.default_deserialized_settings {
+                let default_settings = setting_value.deserialize_setting(default_settings)?;
+
+                user_settings_stack.clear();
+                paths_stack.clear();
+
+                if let Some(user_settings) = &self.user_deserialized_settings {
+                    if let Some(user_settings) =
+                        setting_value.deserialize_setting(user_settings).log_err()
+                    {
+                        user_settings_stack.push(user_settings);
+                        paths_stack.push(None);
+                    }
+                }
+
+                // If the global settings file changed, reload the global value for the field.
+                if changed_local_path.is_none() {
+                    setting_value.set_global_value(setting_value.load_setting(
+                        &default_settings,
+                        &user_settings_stack,
+                        cx,
+                    )?);
+                }
+
+                // Reload the local values for the setting.
+                for (path, local_settings) in &self.local_deserialized_settings {
+                    // Build a stack of all of the local values for that setting.
+                    while let Some(prev_path) = paths_stack.last() {
+                        if let Some(prev_path) = prev_path {
+                            if !path.starts_with(prev_path) {
+                                paths_stack.pop();
+                                user_settings_stack.pop();
+                                continue;
+                            }
+                        }
+                        break;
+                    }
+
+                    if let Some(local_settings) =
+                        setting_value.deserialize_setting(&local_settings).log_err()
+                    {
+                        paths_stack.push(Some(path.as_ref()));
+                        user_settings_stack.push(local_settings);
+
+                        // If a local settings file changed, then avoid recomputing local
+                        // settings for any path outside of that directory.
+                        if changed_local_path.map_or(false, |changed_local_path| {
+                            !path.starts_with(changed_local_path)
+                        }) {
+                            continue;
+                        }
+
+                        setting_value.set_local_value(
+                            path.clone(),
+                            setting_value.load_setting(
+                                &default_settings,
+                                &user_settings_stack,
+                                cx,
+                            )?,
+                        );
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+}
+
+impl<T: Setting> AnySettingValue for SettingValue<T> {
+    fn key(&self) -> Option<&'static str> {
+        T::KEY
+    }
+
+    fn setting_type_name(&self) -> &'static str {
+        type_name::<T>()
+    }
+
+    fn load_setting(
+        &self,
+        default_value: &DeserializedSetting,
+        user_values: &[DeserializedSetting],
+        cx: &AppContext,
+    ) -> Result<Box<dyn Any>> {
+        let default_value = default_value.0.downcast_ref::<T::FileContent>().unwrap();
+        let values: SmallVec<[&T::FileContent; 6]> = user_values
+            .iter()
+            .map(|value| value.0.downcast_ref().unwrap())
+            .collect();
+        Ok(Box::new(T::load(default_value, &values, cx)?))
+    }
+
+    fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result<DeserializedSetting> {
+        if let Some(key) = T::KEY {
+            json = json.get(key).unwrap_or(&serde_json::Value::Null);
+        }
+        let value = T::FileContent::deserialize(json)?;
+        Ok(DeserializedSetting(Box::new(value)))
+    }
+
+    fn value_for_path(&self, path: Option<&Path>) -> &dyn Any {
+        if let Some(path) = path {
+            for (settings_path, value) in self.local_values.iter().rev() {
+                if path.starts_with(&settings_path) {
+                    return value;
+                }
+            }
+        }
+        self.global_value
+            .as_ref()
+            .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name()))
+    }
+
+    fn set_global_value(&mut self, value: Box<dyn Any>) {
+        self.global_value = Some(*value.downcast().unwrap());
+    }
+
+    fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>) {
+        let value = *value.downcast().unwrap();
+        match self.local_values.binary_search_by_key(&&path, |e| &e.0) {
+            Ok(ix) => self.local_values[ix].1 = value,
+            Err(ix) => self.local_values.insert(ix, (path, value)),
+        }
+    }
+
+    fn json_schema(
+        &self,
+        generator: &mut SchemaGenerator,
+        params: &SettingsJsonSchemaParams,
+        cx: &AppContext,
+    ) -> RootSchema {
+        T::json_schema(generator, params, cx)
+    }
+}
+
+// impl Debug for SettingsStore {
+//     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+//         return f
+//             .debug_struct("SettingsStore")
+//             .field(
+//                 "setting_value_sets_by_type",
+//                 &self
+//                     .setting_values
+//                     .values()
+//                     .map(|set| (set.setting_type_name(), set))
+//                     .collect::<HashMap<_, _>>(),
+//             )
+//             .finish_non_exhaustive();
+//     }
+// }
+
+fn update_value_in_json_text<'a>(
+    text: &mut String,
+    key_path: &mut Vec<&'a str>,
+    tab_size: usize,
+    old_value: &'a serde_json::Value,
+    new_value: &'a serde_json::Value,
+    edits: &mut Vec<(Range<usize>, String)>,
+) {
+    // If the old and new values are both objects, then compare them key by key,
+    // preserving the comments and formatting of the unchanged parts. Otherwise,
+    // replace the old value with the new value.
+    if let (serde_json::Value::Object(old_object), serde_json::Value::Object(new_object)) =
+        (old_value, new_value)
+    {
+        for (key, old_sub_value) in old_object.iter() {
+            key_path.push(key);
+            let new_sub_value = new_object.get(key).unwrap_or(&serde_json::Value::Null);
+            update_value_in_json_text(
+                text,
+                key_path,
+                tab_size,
+                old_sub_value,
+                new_sub_value,
+                edits,
+            );
+            key_path.pop();
+        }
+        for (key, new_sub_value) in new_object.iter() {
+            key_path.push(key);
+            if !old_object.contains_key(key) {
+                update_value_in_json_text(
+                    text,
+                    key_path,
+                    tab_size,
+                    &serde_json::Value::Null,
+                    new_sub_value,
+                    edits,
+                );
+            }
+            key_path.pop();
+        }
+    } else if old_value != new_value {
+        let (range, replacement) =
+            replace_value_in_json_text(text, &key_path, tab_size, &new_value);
+        text.replace_range(range.clone(), &replacement);
+        edits.push((range, replacement));
+    }
+}
+
+fn replace_value_in_json_text(
+    text: &str,
+    key_path: &[&str],
+    tab_size: usize,
+    new_value: impl Serialize,
+) -> (Range<usize>, String) {
+    const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
+    const LANGUAGES: &'static str = "languages";
+
+    lazy_static! {
+        static ref PAIR_QUERY: tree_sitter::Query = tree_sitter::Query::new(
+            tree_sitter_json::language(),
+            "(pair key: (string) @key value: (_) @value)",
+        )
+        .unwrap();
+    }
+
+    let mut parser = tree_sitter::Parser::new();
+    parser.set_language(tree_sitter_json::language()).unwrap();
+    let syntax_tree = parser.parse(text, None).unwrap();
+
+    let mut cursor = tree_sitter::QueryCursor::new();
+
+    let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
+
+    let mut depth = 0;
+    let mut last_value_range = 0..0;
+    let mut first_key_start = None;
+    let mut existing_value_range = 0..text.len();
+    let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
+    for mat in matches {
+        if mat.captures.len() != 2 {
+            continue;
+        }
+
+        let key_range = mat.captures[0].node.byte_range();
+        let value_range = mat.captures[1].node.byte_range();
+
+        // Don't enter sub objects until we find an exact
+        // match for the current keypath
+        if last_value_range.contains_inclusive(&value_range) {
+            continue;
+        }
+
+        last_value_range = value_range.clone();
+
+        if key_range.start > existing_value_range.end {
+            break;
+        }
+
+        first_key_start.get_or_insert_with(|| key_range.start);
+
+        let found_key = text
+            .get(key_range.clone())
+            .map(|key_text| {
+                if key_path[depth] == LANGUAGES && has_language_overrides {
+                    return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES);
+                } else {
+                    return key_text == format!("\"{}\"", key_path[depth]);
+                }
+            })
+            .unwrap_or(false);
+
+        if found_key {
+            existing_value_range = value_range;
+            // Reset last value range when increasing in depth
+            last_value_range = existing_value_range.start..existing_value_range.start;
+            depth += 1;
+
+            if depth == key_path.len() {
+                break;
+            } else {
+                first_key_start = None;
+            }
+        }
+    }
+
+    // We found the exact key we want, insert the new value
+    if depth == key_path.len() {
+        let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth);
+        (existing_value_range, new_val)
+    } else {
+        // We have key paths, construct the sub objects
+        let new_key = if has_language_overrides && key_path[depth] == LANGUAGES {
+            LANGUAGE_OVERRIDES
+        } else {
+            key_path[depth]
+        };
+
+        // We don't have the key, construct the nested objects
+        let mut new_value = serde_json::to_value(new_value).unwrap();
+        for key in key_path[(depth + 1)..].iter().rev() {
+            if has_language_overrides && key == &LANGUAGES {
+                new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value });
+            } else {
+                new_value = serde_json::json!({ key.to_string(): new_value });
+            }
+        }
+
+        if let Some(first_key_start) = first_key_start {
+            let mut row = 0;
+            let mut column = 0;
+            for (ix, char) in text.char_indices() {
+                if ix == first_key_start {
+                    break;
+                }
+                if char == '\n' {
+                    row += 1;
+                    column = 0;
+                } else {
+                    column += char.len_utf8();
+                }
+            }
+
+            if row > 0 {
+                // depth is 0 based, but division needs to be 1 based.
+                let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
+                let space = ' ';
+                let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
+                (first_key_start..first_key_start, content)
+            } else {
+                let new_val = serde_json::to_string(&new_value).unwrap();
+                let mut content = format!(r#""{new_key}": {new_val},"#);
+                content.push(' ');
+                (first_key_start..first_key_start, content)
+            }
+        } else {
+            new_value = serde_json::json!({ new_key.to_string(): new_value });
+            let indent_prefix_len = 4 * depth;
+            let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
+            if depth == 0 {
+                new_val.push('\n');
+            }
+
+            (existing_value_range, new_val)
+        }
+    }
+}
+
+fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String {
+    const SPACES: [u8; 32] = [b' '; 32];
+
+    debug_assert!(indent_size <= SPACES.len());
+    debug_assert!(indent_prefix_len <= SPACES.len());
+
+    let mut output = Vec::new();
+    let mut ser = serde_json::Serializer::with_formatter(
+        &mut output,
+        serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
+    );
+
+    value.serialize(&mut ser).unwrap();
+    let text = String::from_utf8(output).unwrap();
+
+    let mut adjusted_text = String::new();
+    for (i, line) in text.split('\n').enumerate() {
+        if i > 0 {
+            adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
+        }
+        adjusted_text.push_str(line);
+        adjusted_text.push('\n');
+    }
+    adjusted_text.pop();
+    adjusted_text
+}
+
+pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
+    Ok(serde_json::from_reader(
+        json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
+    )?)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde_derive::Deserialize;
+    use unindent::Unindent;
+
+    #[gpui::test]
+    fn test_settings_store_basic(cx: &mut AppContext) {
+        let mut store = SettingsStore::default();
+        store.register_setting::<UserSettings>(cx);
+        store.register_setting::<TurboSetting>(cx);
+        store.register_setting::<MultiKeySettings>(cx);
+
+        // error - missing required field in default settings
+        store
+            .set_default_settings(
+                r#"{
+                    "user": {
+                        "name": "John Doe",
+                        "age": 30,
+                        "staff": false
+                    }
+                }"#,
+                cx,
+            )
+            .unwrap_err();
+
+        // error - type error in default settings
+        store
+            .set_default_settings(
+                r#"{
+                    "turbo": "the-wrong-type",
+                    "user": {
+                        "name": "John Doe",
+                        "age": 30,
+                        "staff": false
+                    }
+                }"#,
+                cx,
+            )
+            .unwrap_err();
+
+        // valid default settings.
+        store
+            .set_default_settings(
+                r#"{
+                    "turbo": false,
+                    "user": {
+                        "name": "John Doe",
+                        "age": 30,
+                        "staff": false
+                    }
+                }"#,
+                cx,
+            )
+            .unwrap();
+
+        assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
+        assert_eq!(
+            store.get::<UserSettings>(None),
+            &UserSettings {
+                name: "John Doe".to_string(),
+                age: 30,
+                staff: false,
+            }
+        );
+        assert_eq!(
+            store.get::<MultiKeySettings>(None),
+            &MultiKeySettings {
+                key1: String::new(),
+                key2: String::new(),
+            }
+        );
+
+        store
+            .set_user_settings(
+                r#"{
+                    "turbo": true,
+                    "user": { "age": 31 },
+                    "key1": "a"
+                }"#,
+                cx,
+            )
+            .unwrap();
+
+        assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(true));
+        assert_eq!(
+            store.get::<UserSettings>(None),
+            &UserSettings {
+                name: "John Doe".to_string(),
+                age: 31,
+                staff: false
+            }
+        );
+
+        store
+            .set_local_settings(
+                Path::new("/root1").into(),
+                Some(r#"{ "user": { "staff": true } }"#),
+                cx,
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                Path::new("/root1/subdir").into(),
+                Some(r#"{ "user": { "name": "Jane Doe" } }"#),
+                cx,
+            )
+            .unwrap();
+
+        store
+            .set_local_settings(
+                Path::new("/root2").into(),
+                Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
+                cx,
+            )
+            .unwrap();
+
+        assert_eq!(
+            store.get::<UserSettings>(Some(Path::new("/root1/something"))),
+            &UserSettings {
+                name: "John Doe".to_string(),
+                age: 31,
+                staff: true
+            }
+        );
+        assert_eq!(
+            store.get::<UserSettings>(Some(Path::new("/root1/subdir/something"))),
+            &UserSettings {
+                name: "Jane Doe".to_string(),
+                age: 31,
+                staff: true
+            }
+        );
+        assert_eq!(
+            store.get::<UserSettings>(Some(Path::new("/root2/something"))),
+            &UserSettings {
+                name: "John Doe".to_string(),
+                age: 42,
+                staff: false
+            }
+        );
+        assert_eq!(
+            store.get::<MultiKeySettings>(Some(Path::new("/root2/something"))),
+            &MultiKeySettings {
+                key1: "a".to_string(),
+                key2: "b".to_string(),
+            }
+        );
+    }
+
+    #[gpui::test]
+    fn test_setting_store_assign_json_before_register(cx: &mut AppContext) {
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(
+                r#"{
+                    "turbo": true,
+                    "user": {
+                        "name": "John Doe",
+                        "age": 30,
+                        "staff": false
+                    },
+                    "key1": "x"
+                }"#,
+                cx,
+            )
+            .unwrap();
+        store
+            .set_user_settings(r#"{ "turbo": false }"#, cx)
+            .unwrap();
+        store.register_setting::<UserSettings>(cx);
+        store.register_setting::<TurboSetting>(cx);
+
+        assert_eq!(store.get::<TurboSetting>(None), &TurboSetting(false));
+        assert_eq!(
+            store.get::<UserSettings>(None),
+            &UserSettings {
+                name: "John Doe".to_string(),
+                age: 30,
+                staff: false,
+            }
+        );
+
+        store.register_setting::<MultiKeySettings>(cx);
+        assert_eq!(
+            store.get::<MultiKeySettings>(None),
+            &MultiKeySettings {
+                key1: "x".into(),
+                key2: String::new(),
+            }
+        );
+    }
+
+    #[gpui::test]
+    fn test_setting_store_update(cx: &mut AppContext) {
+        let mut store = SettingsStore::default();
+        store.register_setting::<MultiKeySettings>(cx);
+        store.register_setting::<UserSettings>(cx);
+        store.register_setting::<LanguageSettings>(cx);
+
+        // entries added and updated
+        check_settings_update::<LanguageSettings>(
+            &mut store,
+            r#"{
+                "languages": {
+                    "JSON": {
+                        "is_enabled": true
+                    }
+                }
+            }"#
+            .unindent(),
+            |settings| {
+                settings.languages.get_mut("JSON").unwrap().is_enabled = false;
+                settings
+                    .languages
+                    .insert("Rust".into(), LanguageSettingEntry { is_enabled: true });
+            },
+            r#"{
+                "languages": {
+                    "Rust": {
+                        "is_enabled": true
+                    },
+                    "JSON": {
+                        "is_enabled": false
+                    }
+                }
+            }"#
+            .unindent(),
+            cx,
+        );
+
+        // weird formatting
+        check_settings_update::<UserSettings>(
+            &mut store,
+            r#"{
+                "user":   { "age": 36, "name": "Max", "staff": true }
+            }"#
+            .unindent(),
+            |settings| settings.age = Some(37),
+            r#"{
+                "user":   { "age": 37, "name": "Max", "staff": true }
+            }"#
+            .unindent(),
+            cx,
+        );
+
+        // single-line formatting, other keys
+        check_settings_update::<MultiKeySettings>(
+            &mut store,
+            r#"{ "one": 1, "two": 2 }"#.unindent(),
+            |settings| settings.key1 = Some("x".into()),
+            r#"{ "key1": "x", "one": 1, "two": 2 }"#.unindent(),
+            cx,
+        );
+
+        // empty object
+        check_settings_update::<UserSettings>(
+            &mut store,
+            r#"{
+                "user": {}
+            }"#
+            .unindent(),
+            |settings| settings.age = Some(37),
+            r#"{
+                "user": {
+                    "age": 37
+                }
+            }"#
+            .unindent(),
+            cx,
+        );
+
+        // no content
+        check_settings_update::<UserSettings>(
+            &mut store,
+            r#""#.unindent(),
+            |settings| settings.age = Some(37),
+            r#"{
+                "user": {
+                    "age": 37
+                }
+            }
+            "#
+            .unindent(),
+            cx,
+        );
+    }
+
+    fn check_settings_update<T: Setting>(
+        store: &mut SettingsStore,
+        old_json: String,
+        update: fn(&mut T::FileContent),
+        expected_new_json: String,
+        cx: &mut AppContext,
+    ) {
+        store.set_user_settings(&old_json, cx).ok();
+        let edits = store.edits_for_update::<T>(&old_json, update);
+        let mut new_json = old_json;
+        for (range, replacement) in edits.into_iter() {
+            new_json.replace_range(range, &replacement);
+        }
+        pretty_assertions::assert_eq!(new_json, expected_new_json);
+    }
+
+    #[derive(Debug, PartialEq, Deserialize)]
+    struct UserSettings {
+        name: String,
+        age: u32,
+        staff: bool,
+    }
+
+    #[derive(Clone, Serialize, Deserialize, JsonSchema)]
+    struct UserSettingsJson {
+        name: Option<String>,
+        age: Option<u32>,
+        staff: Option<bool>,
+    }
+
+    impl Setting for UserSettings {
+        const KEY: Option<&'static str> = Some("user");
+        type FileContent = UserSettingsJson;
+
+        fn load(
+            default_value: &UserSettingsJson,
+            user_values: &[&UserSettingsJson],
+            _: &AppContext,
+        ) -> Result<Self> {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct TurboSetting(bool);
+
+    impl Setting for TurboSetting {
+        const KEY: Option<&'static str> = Some("turbo");
+        type FileContent = Option<bool>;
+
+        fn load(
+            default_value: &Option<bool>,
+            user_values: &[&Option<bool>],
+            _: &AppContext,
+        ) -> Result<Self> {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+
+    #[derive(Clone, Debug, PartialEq, Deserialize)]
+    struct MultiKeySettings {
+        #[serde(default)]
+        key1: String,
+        #[serde(default)]
+        key2: String,
+    }
+
+    #[derive(Clone, Serialize, Deserialize, JsonSchema)]
+    struct MultiKeySettingsJson {
+        key1: Option<String>,
+        key2: Option<String>,
+    }
+
+    impl Setting for MultiKeySettings {
+        const KEY: Option<&'static str> = None;
+
+        type FileContent = MultiKeySettingsJson;
+
+        fn load(
+            default_value: &MultiKeySettingsJson,
+            user_values: &[&MultiKeySettingsJson],
+            _: &AppContext,
+        ) -> Result<Self> {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+
+    #[derive(Debug, Deserialize)]
+    struct JournalSettings {
+        pub path: String,
+        pub hour_format: HourFormat,
+    }
+
+    #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+    #[serde(rename_all = "snake_case")]
+    enum HourFormat {
+        Hour12,
+        Hour24,
+    }
+
+    #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+    struct JournalSettingsJson {
+        pub path: Option<String>,
+        pub hour_format: Option<HourFormat>,
+    }
+
+    impl Setting for JournalSettings {
+        const KEY: Option<&'static str> = Some("journal");
+
+        type FileContent = JournalSettingsJson;
+
+        fn load(
+            default_value: &JournalSettingsJson,
+            user_values: &[&JournalSettingsJson],
+            _: &AppContext,
+        ) -> Result<Self> {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+
+    #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+    struct LanguageSettings {
+        #[serde(default)]
+        languages: HashMap<String, LanguageSettingEntry>,
+    }
+
+    #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+    struct LanguageSettingEntry {
+        is_enabled: bool,
+    }
+
+    impl Setting for LanguageSettings {
+        const KEY: Option<&'static str> = None;
+
+        type FileContent = Self;
+
+        fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result<Self> {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+}

crates/settings/src/watched_json.rs 🔗

@@ -1,126 +0,0 @@
-use fs::Fs;
-use futures::StreamExt;
-use gpui::{executor, AppContext};
-use postage::sink::Sink as _;
-use postage::{prelude::Stream, watch};
-use serde::Deserialize;
-
-use std::{path::Path, sync::Arc, time::Duration};
-use theme::ThemeRegistry;
-use util::ResultExt;
-
-use crate::{parse_json_with_comments, KeymapFileContent, Settings, SettingsFileContent};
-
-#[derive(Clone)]
-pub struct WatchedJsonFile<T>(pub watch::Receiver<T>);
-
-impl<T> WatchedJsonFile<T>
-where
-    T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync,
-{
-    pub async fn new(
-        fs: Arc<dyn Fs>,
-        executor: &executor::Background,
-        path: impl Into<Arc<Path>>,
-    ) -> Self {
-        let path = path.into();
-        let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
-        let mut events = fs.watch(&path, Duration::from_millis(500)).await;
-        let (mut tx, rx) = watch::channel_with(settings);
-        executor
-            .spawn(async move {
-                while events.next().await.is_some() {
-                    if let Some(settings) = Self::load(fs.clone(), &path).await {
-                        if tx.send(settings).await.is_err() {
-                            break;
-                        }
-                    }
-                }
-            })
-            .detach();
-        Self(rx)
-    }
-
-    ///Loads the given watched JSON file. In the special case that the file is
-    ///empty (ignoring whitespace) or is not a file, this will return T::default()
-    async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
-        if !fs.is_file(path).await {
-            return Some(T::default());
-        }
-
-        fs.load(path).await.log_err().and_then(|data| {
-            if data.trim().is_empty() {
-                Some(T::default())
-            } else {
-                parse_json_with_comments(&data).log_err()
-            }
-        })
-    }
-
-    pub fn current(&self) -> T {
-        self.0.borrow().clone()
-    }
-}
-
-pub fn watch_files(
-    defaults: Settings,
-    settings_file: WatchedJsonFile<SettingsFileContent>,
-    theme_registry: Arc<ThemeRegistry>,
-    keymap_file: WatchedJsonFile<KeymapFileContent>,
-    cx: &mut AppContext,
-) {
-    watch_settings_file(defaults, settings_file, theme_registry, cx);
-    watch_keymap_file(keymap_file, cx);
-}
-
-pub(crate) fn watch_settings_file(
-    defaults: Settings,
-    mut file: WatchedJsonFile<SettingsFileContent>,
-    theme_registry: Arc<ThemeRegistry>,
-    cx: &mut AppContext,
-) {
-    settings_updated(&defaults, file.0.borrow().clone(), &theme_registry, cx);
-    cx.spawn(|mut cx| async move {
-        while let Some(content) = file.0.recv().await {
-            cx.update(|cx| settings_updated(&defaults, content, &theme_registry, cx));
-        }
-    })
-    .detach();
-}
-
-fn keymap_updated(content: KeymapFileContent, cx: &mut AppContext) {
-    cx.clear_bindings();
-    KeymapFileContent::load_defaults(cx);
-    content.add_to_cx(cx).log_err();
-}
-
-fn settings_updated(
-    defaults: &Settings,
-    content: SettingsFileContent,
-    theme_registry: &Arc<ThemeRegistry>,
-    cx: &mut AppContext,
-) {
-    let mut settings = defaults.clone();
-    settings.set_user_settings(content, theme_registry, cx.font_cache());
-    cx.set_global(settings);
-    cx.refresh_windows();
-}
-
-fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut AppContext) {
-    cx.spawn(|mut cx| async move {
-        let mut settings_subscription = None;
-        while let Some(content) = file.0.recv().await {
-            cx.update(|cx| {
-                let old_base_keymap = cx.global::<Settings>().base_keymap;
-                keymap_updated(content.clone(), cx);
-                settings_subscription = Some(cx.observe_global::<Settings, _>(move |cx| {
-                    let settings = cx.global::<Settings>();
-                    if settings.base_keymap != old_base_keymap {
-                        keymap_updated(content.clone(), cx);
-                    }
-                }));
-            });
-        }
-    })
-    .detach();
-}

crates/terminal/Cargo.toml 🔗

@@ -15,6 +15,7 @@ settings = { path = "../settings" }
 db = { path = "../db" }
 theme = { path = "../theme" }
 util = { path = "../util" }
+
 alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "a51dbe25d67e84d6ed4261e640d3954fbdd9be45" }
 procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
 smallvec.workspace = true
@@ -27,6 +28,7 @@ dirs = "4.0.0"
 shellexpand = "2.1.0"
 libc = "0.2"
 anyhow.workspace = true
+schemars.workspace = true
 thiserror.workspace = true
 lazy_static.workspace = true
 serde.workspace = true

crates/terminal/src/terminal.rs 🔗

@@ -31,8 +31,8 @@ use mappings::mouse::{
 };
 
 use procinfo::LocalProcessInfo;
+use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
 use util::truncate_and_trailoff;
 
 use std::{
@@ -48,11 +48,12 @@ use std::{
 use thiserror::Error;
 
 use gpui::{
+    fonts,
     geometry::vector::{vec2f, Vector2F},
     keymap_matcher::Keystroke,
     platform::{MouseButton, MouseMovedEvent, TouchPhase},
     scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
-    ClipboardItem, Entity, ModelContext, Task,
+    AppContext, ClipboardItem, Entity, ModelContext, Task,
 };
 
 use crate::mappings::{
@@ -114,6 +115,112 @@ impl EventListener for ZedListener {
     }
 }
 
+pub fn init(cx: &mut AppContext) {
+    settings::register::<TerminalSettings>(cx);
+}
+
+#[derive(Deserialize)]
+pub struct TerminalSettings {
+    pub shell: Shell,
+    pub working_directory: WorkingDirectory,
+    font_size: Option<f32>,
+    pub font_family: Option<String>,
+    pub line_height: TerminalLineHeight,
+    pub font_features: Option<fonts::Features>,
+    pub env: HashMap<String, String>,
+    pub blinking: TerminalBlink,
+    pub alternate_scroll: AlternateScroll,
+    pub option_as_meta: bool,
+    pub copy_on_select: bool,
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct TerminalSettingsContent {
+    pub shell: Option<Shell>,
+    pub working_directory: Option<WorkingDirectory>,
+    pub font_size: Option<f32>,
+    pub font_family: Option<String>,
+    pub line_height: Option<TerminalLineHeight>,
+    pub font_features: Option<fonts::Features>,
+    pub env: Option<HashMap<String, String>>,
+    pub blinking: Option<TerminalBlink>,
+    pub alternate_scroll: Option<AlternateScroll>,
+    pub option_as_meta: Option<bool>,
+    pub copy_on_select: Option<bool>,
+}
+
+impl TerminalSettings {
+    pub fn font_size(&self, cx: &AppContext) -> Option<f32> {
+        self.font_size
+            .map(|size| theme::adjusted_font_size(size, cx))
+    }
+}
+
+impl settings::Setting for TerminalSettings {
+    const KEY: Option<&'static str> = Some("terminal");
+
+    type FileContent = TerminalSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &AppContext,
+    ) -> Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalLineHeight {
+    #[default]
+    Comfortable,
+    Standard,
+    Custom(f32),
+}
+
+impl TerminalLineHeight {
+    pub fn value(&self) -> f32 {
+        match self {
+            TerminalLineHeight::Comfortable => 1.618,
+            TerminalLineHeight::Standard => 1.3,
+            TerminalLineHeight::Custom(line_height) => *line_height,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum TerminalBlink {
+    Off,
+    TerminalControlled,
+    On,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum Shell {
+    System,
+    Program(String),
+    WithArguments { program: String, args: Vec<String> },
+}
+
+#[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 {
+    CurrentProjectDirectory,
+    FirstProjectDirectory,
+    AlwaysHome,
+    Always { directory: String },
+}
+
 #[derive(Clone, Copy, Debug, Serialize, Deserialize)]
 pub struct TerminalSize {
     pub cell_width: f32,
@@ -599,7 +706,7 @@ impl Terminal {
         match event {
             InternalEvent::ColorRequest(index, format) => {
                 let color = term.colors()[*index].unwrap_or_else(|| {
-                    let term_style = &cx.global::<Settings>().theme.terminal;
+                    let term_style = &theme::current(cx).terminal;
                     to_alac_rgb(get_color_at_index(index, &term_style))
                 });
                 self.write_to_pty(format(color))
@@ -1049,16 +1156,7 @@ impl Terminal {
     }
 
     pub fn mouse_up(&mut self, e: &MouseUp, origin: Vector2F, cx: &mut ModelContext<Self>) {
-        let settings = cx.global::<Settings>();
-        let copy_on_select = settings
-            .terminal_overrides
-            .copy_on_select
-            .unwrap_or_else(|| {
-                settings
-                    .terminal_defaults
-                    .copy_on_select
-                    .expect("Should be set in defaults")
-            });
+        let setting = settings::get::<TerminalSettings>(cx);
 
         let position = e.position.sub(origin);
         if self.mouse_mode(e.shift) {
@@ -1072,7 +1170,7 @@ impl Terminal {
                 self.pty_tx.notify(bytes);
             }
         } else {
-            if e.button == MouseButton::Left && copy_on_select {
+            if e.button == MouseButton::Left && setting.copy_on_select {
                 self.copy();
             }
 

crates/terminal_view/src/terminal_button.rs 🔗

@@ -5,7 +5,6 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
 };
-use settings::Settings;
 use std::any::TypeId;
 use workspace::{
     dock::{Dock, FocusDock},
@@ -43,7 +42,7 @@ impl View for TerminalButton {
 
         let has_terminals = !project.local_terminal_handles().is_empty();
         let terminal_count = project.local_terminal_handles().len() as i32;
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
 
         Stack::new()
             .with_child(

crates/terminal_view/src/terminal_element.rs 🔗

@@ -16,7 +16,6 @@ use gpui::{
 use itertools::Itertools;
 use language::CursorShape;
 use ordered_float::OrderedFloat;
-use settings::Settings;
 use terminal::{
     alacritty_terminal::{
         ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
@@ -25,9 +24,9 @@ use terminal::{
         term::{cell::Flags, TermMode},
     },
     mappings::colors::convert_color,
-    IndexedCell, Terminal, TerminalContent, TerminalSize,
+    IndexedCell, Terminal, TerminalContent, TerminalSettings, TerminalSize,
 };
-use theme::TerminalStyle;
+use theme::{TerminalStyle, ThemeSettings};
 use util::ResultExt;
 
 use std::{fmt::Debug, ops::RangeInclusive};
@@ -510,38 +509,47 @@ impl TerminalElement {
 
         scene.push_mouse_region(region);
     }
+}
+
+impl Element<TerminalView> for TerminalElement {
+    type LayoutState = LayoutState;
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: gpui::SizeConstraint,
+        view: &mut TerminalView,
+        cx: &mut LayoutContext<TerminalView>,
+    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+        let settings = settings::get::<ThemeSettings>(cx);
+        let terminal_settings = settings::get::<TerminalSettings>(cx);
+
+        //Setup layout information
+        let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
+        let link_style = settings.theme.editor.link_definition;
+        let tooltip_style = settings.theme.tooltip.clone();
 
-    ///Configures a text style from the current settings.
-    pub fn make_text_style(font_cache: &FontCache, settings: &Settings) -> TextStyle {
-        let font_family_name = settings
-            .terminal_overrides
+        let font_cache = cx.font_cache();
+        let font_size = terminal_settings
+            .font_size(cx)
+            .unwrap_or(settings.buffer_font_size(cx));
+        let font_family_name = terminal_settings
             .font_family
             .as_ref()
-            .or(settings.terminal_defaults.font_family.as_ref())
             .unwrap_or(&settings.buffer_font_family_name);
-        let font_features = settings
-            .terminal_overrides
+        let font_features = terminal_settings
             .font_features
             .as_ref()
-            .or(settings.terminal_defaults.font_features.as_ref())
             .unwrap_or(&settings.buffer_font_features);
-
         let family_id = font_cache
             .load_family(&[font_family_name], &font_features)
             .log_err()
             .unwrap_or(settings.buffer_font_family);
-
-        let font_size = settings
-            .terminal_overrides
-            .font_size
-            .or(settings.terminal_defaults.font_size)
-            .unwrap_or(settings.buffer_font_size);
-
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
 
-        TextStyle {
+        let text_style = TextStyle {
             color: settings.theme.editor.text_color,
             font_family_id: family_id,
             font_family_name: font_cache.family_name(family_id).unwrap(),
@@ -549,34 +557,12 @@ impl TerminalElement {
             font_size,
             font_properties: Default::default(),
             underline: Default::default(),
-        }
-    }
-}
-
-impl Element<TerminalView> for TerminalElement {
-    type LayoutState = LayoutState;
-    type PaintState = ();
-
-    fn layout(
-        &mut self,
-        constraint: gpui::SizeConstraint,
-        view: &mut TerminalView,
-        cx: &mut LayoutContext<TerminalView>,
-    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
-        let settings = cx.global::<Settings>();
-        let font_cache = cx.font_cache();
-
-        //Setup layout information
-        let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone.
-        let link_style = settings.theme.editor.link_definition;
-        let tooltip_style = settings.theme.tooltip.clone();
-
-        let text_style = TerminalElement::make_text_style(font_cache, settings);
+        };
         let selection_color = settings.theme.editor.selection.selection;
         let match_color = settings.theme.search.match_background;
         let gutter;
         let dimensions = {
-            let line_height = text_style.font_size * settings.terminal_line_height();
+            let line_height = text_style.font_size * terminal_settings.line_height.value();
             let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
             gutter = cell_width;
 

crates/terminal_view/src/terminal_view.rs 🔗

@@ -2,6 +2,7 @@ mod persistence;
 pub mod terminal_button;
 pub mod terminal_element;
 
+use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
 use context_menu::{ContextMenu, ContextMenuItem};
 use dirs::home_dir;
 use gpui::{
@@ -16,7 +17,6 @@ use gpui::{
 };
 use project::{LocalWorktree, Project};
 use serde::Deserialize;
-use settings::{Settings, TerminalBlink, WorkingDirectory};
 use smallvec::{smallvec, SmallVec};
 use smol::Timer;
 use std::{
@@ -30,7 +30,7 @@ use terminal::{
         index::Point,
         term::{search::RegexSearch, TermMode},
     },
-    Event, Terminal,
+    Event, Terminal, TerminalBlink, WorkingDirectory,
 };
 use util::ResultExt;
 use workspace::{
@@ -41,7 +41,7 @@ use workspace::{
     Pane, ToolbarItemLocation, Workspace, WorkspaceId,
 };
 
-use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
+pub use terminal::TerminalSettings;
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 
@@ -63,6 +63,8 @@ actions!(
 impl_actions!(terminal, [SendText, SendKeystroke]);
 
 pub fn init(cx: &mut AppContext) {
+    terminal::init(cx);
+
     cx.add_action(TerminalView::deploy);
 
     register_deserializable_item::<TerminalView>(cx);
@@ -101,9 +103,9 @@ impl TerminalView {
         _: &workspace::NewTerminal,
         cx: &mut ViewContext<Workspace>,
     ) {
-        let strategy = cx.global::<Settings>().terminal_strategy();
-
-        let working_directory = get_working_directory(workspace, cx, strategy);
+        let strategy = settings::get::<TerminalSettings>(cx);
+        let working_directory =
+            get_working_directory(workspace, cx, strategy.working_directory.clone());
 
         let window_id = cx.window_id();
         let terminal = workspace
@@ -215,10 +217,7 @@ impl TerminalView {
             self.terminal.update(cx, |term, cx| {
                 term.try_keystroke(
                     &Keystroke::parse("ctrl-cmd-space").unwrap(),
-                    cx.global::<Settings>()
-                        .terminal_overrides
-                        .option_as_meta
-                        .unwrap_or(false),
+                    settings::get::<TerminalSettings>(cx).option_as_meta,
                 )
             });
         }
@@ -244,16 +243,7 @@ impl TerminalView {
             return true;
         }
 
-        let setting = {
-            let settings = cx.global::<Settings>();
-            settings
-                .terminal_overrides
-                .blinking
-                .clone()
-                .unwrap_or(TerminalBlink::TerminalControlled)
-        };
-
-        match setting {
+        match settings::get::<TerminalSettings>(cx).blinking {
             //If the user requested to never blink, don't blink it.
             TerminalBlink::Off => true,
             //If the terminal is controlling it, check terminal mode
@@ -346,10 +336,7 @@ impl TerminalView {
             self.terminal.update(cx, |term, cx| {
                 term.try_keystroke(
                     &keystroke,
-                    cx.global::<Settings>()
-                        .terminal_overrides
-                        .option_as_meta
-                        .unwrap_or(false),
+                    settings::get::<TerminalSettings>(cx).option_as_meta,
                 );
             });
         }
@@ -412,10 +399,7 @@ impl View for TerminalView {
         self.terminal.update(cx, |term, cx| {
             term.try_keystroke(
                 &event.keystroke,
-                cx.global::<Settings>()
-                    .terminal_overrides
-                    .option_as_meta
-                    .unwrap_or(false),
+                settings::get::<TerminalSettings>(cx).option_as_meta,
             )
         })
     }
@@ -617,7 +601,9 @@ impl Item for TerminalView {
                 .flatten()
                 .or_else(|| {
                     cx.read(|cx| {
-                        let strategy = cx.global::<Settings>().terminal_strategy();
+                        let strategy = settings::get::<TerminalSettings>(cx)
+                            .working_directory
+                            .clone();
                         workspace
                             .upgrade(cx)
                             .map(|workspace| {
@@ -801,22 +787,18 @@ fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
 
 #[cfg(test)]
 mod tests {
-
     use super::*;
     use gpui::TestAppContext;
     use project::{Entry, Project, ProjectPath, Worktree};
-    use workspace::AppState;
-
     use std::path::Path;
+    use workspace::AppState;
 
-    ///Working directory calculation tests
+    // Working directory calculation tests
 
-    ///No Worktrees in project -> home_dir()
+    // No Worktrees in project -> home_dir()
     #[gpui::test]
     async fn no_worktree(cx: &mut TestAppContext) {
-        //Setup variables
-        let (project, workspace) = blank_workspace(cx).await;
-        //Test
+        let (project, workspace) = init_test(cx).await;
         cx.read(|cx| {
             let workspace = workspace.read(cx);
             let active_entry = project.read(cx).active_entry();
@@ -832,14 +814,12 @@ mod tests {
         });
     }
 
-    ///No active entry, but a worktree, worktree is a file -> home_dir()
+    // No active entry, but a worktree, worktree is a file -> home_dir()
     #[gpui::test]
     async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
-        //Setup variables
+        let (project, workspace) = init_test(cx).await;
 
-        let (project, workspace) = blank_workspace(cx).await;
         create_file_wt(project.clone(), "/root.txt", cx).await;
-
         cx.read(|cx| {
             let workspace = workspace.read(cx);
             let active_entry = project.read(cx).active_entry();
@@ -855,14 +835,12 @@ mod tests {
         });
     }
 
-    //No active entry, but a worktree, worktree is a folder -> worktree_folder
+    // No active entry, but a worktree, worktree is a folder -> worktree_folder
     #[gpui::test]
     async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
-        //Setup variables
-        let (project, workspace) = blank_workspace(cx).await;
-        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
+        let (project, workspace) = init_test(cx).await;
 
-        //Test
+        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
         cx.update(|cx| {
             let workspace = workspace.read(cx);
             let active_entry = project.read(cx).active_entry();
@@ -877,17 +855,15 @@ mod tests {
         });
     }
 
-    //Active entry with a work tree, worktree is a file -> home_dir()
+    // Active entry with a work tree, worktree is a file -> home_dir()
     #[gpui::test]
     async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
-        //Setup variables
+        let (project, workspace) = init_test(cx).await;
 
-        let (project, workspace) = blank_workspace(cx).await;
         let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
         let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
         insert_active_entry_for(wt2, entry2, project.clone(), cx);
 
-        //Test
         cx.update(|cx| {
             let workspace = workspace.read(cx);
             let active_entry = project.read(cx).active_entry();
@@ -901,16 +877,15 @@ mod tests {
         });
     }
 
-    //Active entry, with a worktree, worktree is a folder -> worktree_folder
+    // Active entry, with a worktree, worktree is a folder -> worktree_folder
     #[gpui::test]
     async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
-        //Setup variables
-        let (project, workspace) = blank_workspace(cx).await;
+        let (project, workspace) = init_test(cx).await;
+
         let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
         let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
         insert_active_entry_for(wt2, entry2, project.clone(), cx);
 
-        //Test
         cx.update(|cx| {
             let workspace = workspace.read(cx);
             let active_entry = project.read(cx).active_entry();
@@ -924,11 +899,12 @@ mod tests {
         });
     }
 
-    ///Creates a worktree with 1 file: /root.txt
-    pub async fn blank_workspace(
+    /// Creates a worktree with 1 file: /root.txt
+    pub async fn init_test(
         cx: &mut TestAppContext,
     ) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
         let params = cx.update(AppState::test);
+        cx.update(|cx| theme::init((), cx));
 
         let project = Project::test(params.fs.clone(), [], cx).await;
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
@@ -936,7 +912,7 @@ mod tests {
         (project, workspace)
     }
 
-    ///Creates a worktree with 1 folder: /root{suffix}/
+    /// Creates a worktree with 1 folder: /root{suffix}/
     async fn create_folder_wt(
         project: ModelHandle<Project>,
         path: impl AsRef<Path>,
@@ -945,7 +921,7 @@ mod tests {
         create_wt(project, true, path, cx).await
     }
 
-    ///Creates a worktree with 1 file: /root{suffix}.txt
+    /// Creates a worktree with 1 file: /root{suffix}.txt
     async fn create_file_wt(
         project: ModelHandle<Project>,
         path: impl AsRef<Path>,

crates/theme/Cargo.toml 🔗

@@ -4,6 +4,13 @@ version = "0.1.0"
 edition = "2021"
 publish = false
 
+[features]
+test-support = [
+    "gpui/test-support",
+    "fs/test-support",
+    "settings/test-support"
+]
+
 [lib]
 path = "src/theme.rs"
 doctest = false
@@ -11,10 +18,19 @@ doctest = false
 [dependencies]
 gpui = { path = "../gpui" }
 fs = { path = "../fs" }
+settings = { path = "../settings" }
+util = { path = "../util" }
+
 anyhow.workspace = true
 indexmap = "1.6.2"
 parking_lot.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-toml = "0.5"
+toml.workspace = true
+
+[dev-dependencies]
+gpui = { path = "../gpui", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }

crates/theme/src/theme.rs 🔗

@@ -1,19 +1,40 @@
 mod theme_registry;
+mod theme_settings;
+pub mod ui;
 
 use gpui::{
     color::Color,
     elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, TooltipStyle},
     fonts::{HighlightStyle, TextStyle},
-    platform, Border, MouseState,
+    platform, AppContext, AssetSource, Border, MouseState,
 };
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
+use settings::SettingsStore;
 use std::{collections::HashMap, sync::Arc};
 use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle};
 
-pub mod ui;
-
 pub use theme_registry::*;
+pub use theme_settings::*;
+
+pub fn current(cx: &AppContext) -> Arc<Theme> {
+    settings::get::<ThemeSettings>(cx).theme.clone()
+}
+
+pub fn init(source: impl AssetSource, cx: &mut AppContext) {
+    cx.set_global(ThemeRegistry::new(source, cx.font_cache().clone()));
+    settings::register::<ThemeSettings>(cx);
+
+    let mut prev_buffer_font_size = settings::get::<ThemeSettings>(cx).buffer_font_size;
+    cx.observe_global::<SettingsStore, _>(move |cx| {
+        let buffer_font_size = settings::get::<ThemeSettings>(cx).buffer_font_size;
+        if buffer_font_size != prev_buffer_font_size {
+            prev_buffer_font_size = buffer_font_size;
+            reset_font_size(cx);
+        }
+    })
+    .detach();
+}
 
 #[derive(Deserialize, Default)]
 pub struct Theme {

crates/theme/src/theme_registry.rs 🔗

@@ -22,13 +22,26 @@ pub struct ThemeRegistry {
 
 impl ThemeRegistry {
     pub fn new(source: impl AssetSource, font_cache: Arc<FontCache>) -> Arc<Self> {
-        Arc::new(Self {
+        let this = Arc::new(Self {
             assets: Box::new(source),
             themes: Default::default(),
             theme_data: Default::default(),
             next_theme_id: Default::default(),
             font_cache,
-        })
+        });
+
+        #[cfg(any(test, feature = "test-support"))]
+        this.themes.lock().insert(
+            settings::EMPTY_THEME_NAME.to_string(),
+            gpui::fonts::with_font_cache(this.font_cache.clone(), || {
+                let mut theme = Theme::default();
+                theme.meta.id = this.next_theme_id.fetch_add(1, SeqCst);
+                theme.meta.name = settings::EMPTY_THEME_NAME.into();
+                Arc::new(theme)
+            }),
+        );
+
+        this
     }
 
     pub fn list(&self, staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {

crates/theme/src/theme_settings.rs 🔗

@@ -0,0 +1,184 @@
+use crate::{Theme, ThemeRegistry};
+use anyhow::Result;
+use gpui::{font_cache::FamilyId, fonts, AppContext};
+use schemars::{
+    gen::SchemaGenerator,
+    schema::{InstanceType, Schema, SchemaObject},
+    JsonSchema,
+};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use settings::SettingsJsonSchemaParams;
+use std::sync::Arc;
+use util::ResultExt as _;
+
+const MIN_FONT_SIZE: f32 = 6.0;
+
+#[derive(Clone)]
+pub struct ThemeSettings {
+    pub buffer_font_family_name: String,
+    pub buffer_font_features: fonts::Features,
+    pub buffer_font_family: FamilyId,
+    pub(crate) buffer_font_size: f32,
+    pub theme: Arc<Theme>,
+}
+
+pub struct AdjustedBufferFontSize(pub f32);
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct ThemeSettingsContent {
+    #[serde(default)]
+    pub buffer_font_family: Option<String>,
+    #[serde(default)]
+    pub buffer_font_size: Option<f32>,
+    #[serde(default)]
+    pub buffer_font_features: Option<fonts::Features>,
+    #[serde(default)]
+    pub theme: Option<String>,
+}
+
+impl ThemeSettings {
+    pub fn buffer_font_size(&self, cx: &AppContext) -> f32 {
+        if cx.has_global::<AdjustedBufferFontSize>() {
+            cx.global::<AdjustedBufferFontSize>().0
+        } else {
+            self.buffer_font_size
+        }
+        .max(MIN_FONT_SIZE)
+    }
+}
+
+pub fn adjusted_font_size(size: f32, cx: &AppContext) -> f32 {
+    if cx.has_global::<AdjustedBufferFontSize>() {
+        let buffer_font_size = settings::get::<ThemeSettings>(cx).buffer_font_size;
+        let delta = cx.global::<AdjustedBufferFontSize>().0 - buffer_font_size;
+        size + delta
+    } else {
+        size
+    }
+    .max(MIN_FONT_SIZE)
+}
+
+pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut f32)) {
+    if !cx.has_global::<AdjustedBufferFontSize>() {
+        let buffer_font_size = settings::get::<ThemeSettings>(cx).buffer_font_size;
+        cx.set_global(AdjustedBufferFontSize(buffer_font_size));
+    }
+
+    cx.update_global::<AdjustedBufferFontSize, _, _>(|delta, cx| {
+        f(&mut delta.0);
+        delta.0 = delta
+            .0
+            .max(MIN_FONT_SIZE - settings::get::<ThemeSettings>(cx).buffer_font_size);
+    });
+    cx.refresh_windows();
+}
+
+pub fn reset_font_size(cx: &mut AppContext) {
+    if cx.has_global::<AdjustedBufferFontSize>() {
+        cx.remove_global::<AdjustedBufferFontSize>();
+        cx.refresh_windows();
+    }
+}
+
+impl settings::Setting for ThemeSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = ThemeSettingsContent;
+
+    fn load(
+        defaults: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        cx: &AppContext,
+    ) -> Result<Self> {
+        let buffer_font_features = defaults.buffer_font_features.clone().unwrap();
+        let themes = cx.global::<Arc<ThemeRegistry>>();
+
+        let mut this = Self {
+            buffer_font_family: cx
+                .font_cache()
+                .load_family(
+                    &[defaults.buffer_font_family.as_ref().unwrap()],
+                    &buffer_font_features,
+                )
+                .unwrap(),
+            buffer_font_family_name: defaults.buffer_font_family.clone().unwrap(),
+            buffer_font_features,
+            buffer_font_size: defaults.buffer_font_size.unwrap(),
+            theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(),
+        };
+
+        for value in user_values.into_iter().copied().cloned() {
+            let font_cache = cx.font_cache();
+            let mut family_changed = false;
+            if let Some(value) = value.buffer_font_family {
+                this.buffer_font_family_name = value;
+                family_changed = true;
+            }
+            if let Some(value) = value.buffer_font_features {
+                this.buffer_font_features = value;
+                family_changed = true;
+            }
+            if family_changed {
+                if let Some(id) = font_cache
+                    .load_family(&[&this.buffer_font_family_name], &this.buffer_font_features)
+                    .log_err()
+                {
+                    this.buffer_font_family = id;
+                }
+            }
+
+            if let Some(value) = &value.theme {
+                if let Some(theme) = themes.get(value).log_err() {
+                    this.theme = theme;
+                }
+            }
+
+            merge(&mut this.buffer_font_size, value.buffer_font_size);
+        }
+
+        Ok(this)
+    }
+
+    fn json_schema(
+        generator: &mut SchemaGenerator,
+        params: &SettingsJsonSchemaParams,
+        cx: &AppContext,
+    ) -> schemars::schema::RootSchema {
+        let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
+        let theme_names = cx
+            .global::<Arc<ThemeRegistry>>()
+            .list(params.staff_mode)
+            .map(|theme| Value::String(theme.name.clone()))
+            .collect();
+
+        let theme_name_schema = SchemaObject {
+            instance_type: Some(InstanceType::String.into()),
+            enum_values: Some(theme_names),
+            ..Default::default()
+        };
+
+        root_schema
+            .definitions
+            .extend([("ThemeName".into(), theme_name_schema.into())]);
+
+        root_schema
+            .schema
+            .object
+            .as_mut()
+            .unwrap()
+            .properties
+            .extend([(
+                "theme".to_owned(),
+                Schema::new_ref("#/definitions/ThemeName".into()),
+            )]);
+
+        root_schema
+    }
+}
+
+fn merge<T: Copy>(target: &mut T, value: Option<T>) {
+    if let Some(value) = value {
+        *target = value;
+    }
+}

crates/theme_selector/Cargo.toml 🔗

@@ -11,6 +11,7 @@ doctest = false
 [dependencies]
 editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
+fs = { path = "../fs" }
 gpui = { path = "../gpui" }
 picker = { path = "../picker" }
 theme = { path = "../theme" }

crates/theme_selector/src/theme_selector.rs 🔗

@@ -1,10 +1,11 @@
+use fs::Fs;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext};
 use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::{settings_file::SettingsFile, Settings};
+use settings::{update_settings_file, SettingsStore};
 use staff_mode::StaffMode;
 use std::sync::Arc;
-use theme::{Theme, ThemeMeta, ThemeRegistry};
+use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
 use util::ResultExt;
 use workspace::Workspace;
 
@@ -17,16 +18,17 @@ pub fn init(cx: &mut AppContext) {
 
 pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
     workspace.toggle_modal(cx, |workspace, cx| {
-        let themes = workspace.app_state().themes.clone();
-        cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx))
+        let fs = workspace.app_state().fs.clone();
+        cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(fs, cx), cx))
     });
 }
 
 #[cfg(debug_assertions)]
-pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut AppContext) {
-    let current_theme_name = cx.global::<Settings>().theme.meta.name.clone();
-    themes.clear();
-    match themes.get(&current_theme_name) {
+pub fn reload(cx: &mut AppContext) {
+    let current_theme_name = theme::current(cx).meta.name.clone();
+    let registry = cx.global::<Arc<ThemeRegistry>>();
+    registry.clear();
+    match registry.get(&current_theme_name) {
         Ok(theme) => {
             ThemeSelectorDelegate::set_theme(theme, cx);
             log::info!("reloaded theme {}", current_theme_name);
@@ -40,7 +42,7 @@ pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut AppContext) {
 pub type ThemeSelector = Picker<ThemeSelectorDelegate>;
 
 pub struct ThemeSelectorDelegate {
-    registry: Arc<ThemeRegistry>,
+    fs: Arc<dyn Fs>,
     theme_data: Vec<ThemeMeta>,
     matches: Vec<StringMatch>,
     original_theme: Arc<Theme>,
@@ -49,14 +51,12 @@ pub struct ThemeSelectorDelegate {
 }
 
 impl ThemeSelectorDelegate {
-    fn new(registry: Arc<ThemeRegistry>, cx: &mut ViewContext<ThemeSelector>) -> Self {
-        let settings = cx.global::<Settings>();
+    fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<ThemeSelector>) -> Self {
+        let original_theme = theme::current(cx).clone();
 
-        let original_theme = settings.theme.clone();
-
-        let mut theme_names = registry
-            .list(**cx.default_global::<StaffMode>())
-            .collect::<Vec<_>>();
+        let staff_mode = **cx.default_global::<StaffMode>();
+        let registry = cx.global::<Arc<ThemeRegistry>>();
+        let mut theme_names = registry.list(staff_mode).collect::<Vec<_>>();
         theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
         let matches = theme_names
             .iter()
@@ -68,7 +68,7 @@ impl ThemeSelectorDelegate {
             })
             .collect();
         let mut this = Self {
-            registry,
+            fs,
             theme_data: theme_names,
             matches,
             original_theme: original_theme.clone(),
@@ -81,7 +81,8 @@ impl ThemeSelectorDelegate {
 
     fn show_selected_theme(&mut self, cx: &mut ViewContext<ThemeSelector>) {
         if let Some(mat) = self.matches.get(self.selected_index) {
-            match self.registry.get(&mat.string) {
+            let registry = cx.global::<Arc<ThemeRegistry>>();
+            match registry.get(&mat.string) {
                 Ok(theme) => {
                     Self::set_theme(theme, cx);
                 }
@@ -101,8 +102,10 @@ impl ThemeSelectorDelegate {
     }
 
     fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
-        cx.update_global::<Settings, _, _>(|settings, cx| {
-            settings.theme = theme;
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            let mut theme_settings = store.get::<ThemeSettings>(None).clone();
+            theme_settings.theme = theme;
+            store.override_global(theme_settings);
             cx.refresh_windows();
         });
     }
@@ -120,9 +123,9 @@ impl PickerDelegate for ThemeSelectorDelegate {
     fn confirm(&mut self, cx: &mut ViewContext<ThemeSelector>) {
         self.selection_completed = true;
 
-        let theme_name = cx.global::<Settings>().theme.meta.name.clone();
-        SettingsFile::update(cx, |settings_content| {
-            settings_content.theme = Some(theme_name);
+        let theme_name = theme::current(cx).meta.name.clone();
+        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, |settings| {
+            settings.theme = Some(theme_name);
         });
 
         cx.emit(PickerEvent::Dismiss);
@@ -204,11 +207,10 @@ impl PickerDelegate for ThemeSelectorDelegate {
         selected: bool,
         cx: &AppContext,
     ) -> AnyElement<Picker<Self>> {
-        let settings = cx.global::<Settings>();
-        let theme = &settings.theme;
-        let theme_match = &self.matches[ix];
+        let theme = theme::current(cx);
         let style = theme.picker.item.style_for(mouse_state, selected);
 
+        let theme_match = &self.matches[ix];
         Label::new(theme_match.string.clone(), style.label.clone())
             .with_highlights(theme_match.positions.clone())
             .contained()

crates/theme_testbench/src/theme_testbench.rs 🔗

@@ -10,8 +10,7 @@ use gpui::{
     WeakViewHandle,
 };
 use project::Project;
-use settings::Settings;
-use theme::{ColorScheme, Layer, Style, StyleSet};
+use theme::{ColorScheme, Layer, Style, StyleSet, ThemeSettings};
 use workspace::{item::Item, register_deserializable_item, Pane, Workspace};
 
 actions!(theme, [DeployThemeTestbench]);
@@ -220,10 +219,10 @@ impl ThemeTestbench {
     }
 
     fn render_label(text: String, style: &Style, cx: &mut ViewContext<Self>) -> Label {
-        let settings = cx.global::<Settings>();
+        let settings = settings::get::<ThemeSettings>(cx);
         let font_cache = cx.font_cache();
         let family_id = settings.buffer_font_family;
-        let font_size = settings.buffer_font_size;
+        let font_size = settings.buffer_font_size(cx);
         let font_id = font_cache
             .select_font(family_id, &Default::default())
             .unwrap();
@@ -252,7 +251,7 @@ impl View for ThemeTestbench {
     }
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
-        let color_scheme = &cx.global::<Settings>().theme.clone().color_scheme;
+        let color_scheme = &theme::current(cx).clone().color_scheme;
 
         Flex::row()
             .with_child(

crates/util/src/util.rs 🔗

@@ -95,6 +95,27 @@ pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json:
     }
 }
 
+pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) {
+    use serde_json::Value;
+    if let Value::Object(source_object) = source {
+        let target_object = if let Value::Object(target) = target {
+            target
+        } else {
+            *target = Value::Object(Default::default());
+            target.as_object_mut().unwrap()
+        };
+        for (key, value) in source_object {
+            if let Some(target) = target_object.get_mut(&key) {
+                merge_non_null_json_value_into(value, target);
+            } else if !value.is_null() {
+                target_object.insert(key.clone(), value);
+            }
+        }
+    } else if !source.is_null() {
+        *target = source
+    }
+}
+
 pub trait ResultExt {
     type Ok;
 

crates/vim/Cargo.toml 🔗

@@ -12,6 +12,7 @@ doctest = false
 neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"]
 
 [dependencies]
+anyhow.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 itertools = "0.10"

crates/vim/src/test/vim_test_context.rs 🔗

@@ -17,14 +17,17 @@ pub struct VimTestContext<'a> {
 impl<'a> VimTestContext<'a> {
     pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> {
         let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
+
         cx.update(|cx| {
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.vim_mode = enabled;
-            });
             search::init(cx);
             crate::init(cx);
+        });
 
-            settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
+        cx.update(|cx| {
+            cx.update_global(|store: &mut SettingsStore, cx| {
+                store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
+            });
+            settings::KeymapFileContent::load_asset("keymaps/vim.json", cx).unwrap();
         });
 
         // Setup search toolbars and keypress hook
@@ -52,16 +55,16 @@ impl<'a> VimTestContext<'a> {
 
     pub fn enable_vim(&mut self) {
         self.cx.update(|cx| {
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.vim_mode = true;
+            cx.update_global(|store: &mut SettingsStore, cx| {
+                store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(true));
             });
         })
     }
 
     pub fn disable_vim(&mut self) {
         self.cx.update(|cx| {
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.vim_mode = false;
+            cx.update_global(|store: &mut SettingsStore, cx| {
+                store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(false));
             });
         })
     }

crates/vim/src/vim.rs 🔗

@@ -10,8 +10,7 @@ mod state;
 mod utils;
 mod visual;
 
-use std::sync::Arc;
-
+use anyhow::Result;
 use collections::CommandPaletteFilter;
 use editor::{Bias, Cancel, Editor, EditorMode, Event};
 use gpui::{
@@ -22,11 +21,14 @@ use language::CursorShape;
 use motion::Motion;
 use normal::normal_replace;
 use serde::Deserialize;
-use settings::Settings;
+use settings::{Setting, SettingsStore};
 use state::{Mode, Operator, VimState};
+use std::sync::Arc;
 use visual::visual_replace;
 use workspace::{self, Workspace};
 
+struct VimModeSetting(bool);
+
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct SwitchMode(pub Mode);
 
@@ -40,6 +42,8 @@ actions!(vim, [Tab, Enter]);
 impl_actions!(vim, [Number, SwitchMode, PushOperator]);
 
 pub fn init(cx: &mut AppContext) {
+    settings::register::<VimModeSetting>(cx);
+
     editor_events::init(cx);
     normal::init(cx);
     visual::init(cx);
@@ -91,11 +95,11 @@ pub fn init(cx: &mut AppContext) {
         filter.filtered_namespaces.insert("vim");
     });
     cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
-        vim.set_enabled(cx.global::<Settings>().vim_mode, cx)
+        vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
     });
-    cx.observe_global::<Settings, _>(|cx| {
+    cx.observe_global::<SettingsStore, _>(|cx| {
         cx.update_default_global(|vim: &mut Vim, cx: &mut AppContext| {
-            vim.set_enabled(cx.global::<Settings>().vim_mode, cx)
+            vim.set_enabled(settings::get::<VimModeSetting>(cx).0, cx)
         });
     })
     .detach();
@@ -330,6 +334,22 @@ impl Vim {
     }
 }
 
+impl Setting for VimModeSetting {
+    const KEY: Option<&'static str> = Some("vim_mode");
+
+    type FileContent = Option<bool>;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &AppContext,
+    ) -> Result<Self> {
+        Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or(
+            default_value.ok_or_else(Self::missing_default)?,
+        )))
+    }
+}
+
 fn local_selections_changed(newest_empty: bool, cx: &mut WindowContext) {
     Vim::update(cx, |vim, cx| {
         if vim.enabled && vim.state.mode == Mode::Normal && !newest_empty {

crates/welcome/Cargo.toml 🔗

@@ -11,9 +11,9 @@ path = "src/welcome.rs"
 test-support = []
 
 [dependencies]
-anyhow.workspace = true
-log.workspace = true
+client = { path = "../client" }
 editor = { path = "../editor" }
+fs = { path = "../fs" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 db = { path = "../db" }
@@ -25,3 +25,8 @@ theme_selector = { path = "../theme_selector" }
 util = { path = "../util" }
 picker = { path = "../picker" }
 workspace = { path = "../workspace" }
+
+anyhow.workspace = true
+log.workspace = true
+schemars.workspace = true
+serde.workspace = true

crates/welcome/src/base_keymap_picker.rs 🔗

@@ -1,5 +1,4 @@
-use std::sync::Arc;
-
+use super::base_keymap_setting::BaseKeymap;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
     actions,
@@ -7,7 +6,9 @@ use gpui::{
     AppContext, Task, ViewContext,
 };
 use picker::{Picker, PickerDelegate, PickerEvent};
-use settings::{settings_file::SettingsFile, BaseKeymap, Settings};
+use project::Fs;
+use settings::update_settings_file;
+use std::sync::Arc;
 use util::ResultExt;
 use workspace::Workspace;
 
@@ -23,8 +24,9 @@ pub fn toggle(
     _: &ToggleBaseKeymapSelector,
     cx: &mut ViewContext<Workspace>,
 ) {
-    workspace.toggle_modal(cx, |_, cx| {
-        cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(cx), cx))
+    workspace.toggle_modal(cx, |workspace, cx| {
+        let fs = workspace.app_state().fs.clone();
+        cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(fs, cx), cx))
     });
 }
 
@@ -33,18 +35,20 @@ pub type BaseKeymapSelector = Picker<BaseKeymapSelectorDelegate>;
 pub struct BaseKeymapSelectorDelegate {
     matches: Vec<StringMatch>,
     selected_index: usize,
+    fs: Arc<dyn Fs>,
 }
 
 impl BaseKeymapSelectorDelegate {
-    fn new(cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
-        let base = cx.global::<Settings>().base_keymap;
+    fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
+        let base = settings::get::<BaseKeymap>(cx);
         let selected_index = BaseKeymap::OPTIONS
             .iter()
-            .position(|(_, value)| *value == base)
+            .position(|(_, value)| value == base)
             .unwrap_or(0);
         Self {
             matches: Vec::new(),
             selected_index,
+            fs,
         }
     }
 }
@@ -119,7 +123,9 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
     fn confirm(&mut self, cx: &mut ViewContext<BaseKeymapSelector>) {
         if let Some(selection) = self.matches.get(self.selected_index) {
             let base_keymap = BaseKeymap::from_names(&selection.string);
-            SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap));
+            update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
+                *setting = Some(base_keymap)
+            });
         }
         cx.emit(PickerEvent::Dismiss);
     }
@@ -133,7 +139,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
         selected: bool,
         cx: &gpui::AppContext,
     ) -> gpui::AnyElement<Picker<Self>> {
-        let theme = &cx.global::<Settings>().theme;
+        let theme = &theme::current(cx);
         let keymap_match = &self.matches[ix];
         let style = theme.picker.item.style_for(mouse_state, selected);
 

crates/welcome/src/base_keymap_setting.rs 🔗

@@ -0,0 +1,65 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
+pub enum BaseKeymap {
+    #[default]
+    VSCode,
+    JetBrains,
+    SublimeText,
+    Atom,
+    TextMate,
+}
+
+impl BaseKeymap {
+    pub const OPTIONS: [(&'static str, Self); 5] = [
+        ("VSCode (Default)", Self::VSCode),
+        ("Atom", Self::Atom),
+        ("JetBrains", Self::JetBrains),
+        ("Sublime Text", Self::SublimeText),
+        ("TextMate", Self::TextMate),
+    ];
+
+    pub fn asset_path(&self) -> Option<&'static str> {
+        match self {
+            BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
+            BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
+            BaseKeymap::Atom => Some("keymaps/atom.json"),
+            BaseKeymap::TextMate => Some("keymaps/textmate.json"),
+            BaseKeymap::VSCode => None,
+        }
+    }
+
+    pub fn names() -> impl Iterator<Item = &'static str> {
+        Self::OPTIONS.iter().map(|(name, _)| *name)
+    }
+
+    pub fn from_names(option: &str) -> BaseKeymap {
+        Self::OPTIONS
+            .iter()
+            .copied()
+            .find_map(|(name, value)| (name == option).then(|| value))
+            .unwrap_or_default()
+    }
+}
+
+impl Setting for BaseKeymap {
+    const KEY: Option<&'static str> = Some("base_keymap");
+
+    type FileContent = Option<Self>;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self>
+    where
+        Self: Sized,
+    {
+        Ok(user_values
+            .first()
+            .and_then(|v| **v)
+            .unwrap_or(default_value.unwrap()))
+    }
+}

crates/welcome/src/welcome.rs 🔗

@@ -1,24 +1,27 @@
 mod base_keymap_picker;
+mod base_keymap_setting;
 
-use std::{borrow::Cow, sync::Arc};
-
+use crate::base_keymap_picker::ToggleBaseKeymapSelector;
+use client::TelemetrySettings;
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
     elements::{Flex, Label, ParentElement},
     AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
 };
-use settings::{settings_file::SettingsFile, Settings};
-
+use settings::{update_settings_file, SettingsStore};
+use std::{borrow::Cow, sync::Arc};
 use workspace::{
     item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
     WorkspaceId,
 };
 
-use crate::base_keymap_picker::ToggleBaseKeymapSelector;
+pub use base_keymap_setting::BaseKeymap;
 
 pub const FIRST_OPEN: &str = "first_open";
 
 pub fn init(cx: &mut AppContext) {
+    settings::register::<BaseKeymap>(cx);
+
     cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
         let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
         workspace.add_item(Box::new(welcome_page), cx)
@@ -58,15 +61,10 @@ impl View for WelcomePage {
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
         let self_handle = cx.handle();
-        let settings = cx.global::<Settings>();
-        let theme = settings.theme.clone();
-
+        let theme = theme::current(cx);
         let width = theme.welcome.page_width;
 
-        let (diagnostics, metrics) = {
-            let telemetry = settings.telemetry();
-            (telemetry.diagnostics(), telemetry.metrics())
-        };
+        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
 
         enum Metrics {}
         enum Diagnostics {}
@@ -166,13 +164,18 @@ impl View for WelcomePage {
                                         .with_style(theme.welcome.usage_note.container),
                                     ),
                                 &theme.welcome.checkbox,
-                                metrics,
+                                telemetry_settings.metrics,
                                 0,
                                 cx,
-                                |_, checked, cx| {
-                                    SettingsFile::update(cx, move |file| {
-                                        file.telemetry.set_metrics(checked)
-                                    })
+                                |this, checked, cx| {
+                                    if let Some(workspace) = this.workspace.upgrade(cx) {
+                                        let fs = workspace.read(cx).app_state().fs.clone();
+                                        update_settings_file::<TelemetrySettings>(
+                                            fs,
+                                            cx,
+                                            move |setting| setting.metrics = Some(checked),
+                                        )
+                                    }
                                 },
                             )
                             .contained()
@@ -182,13 +185,18 @@ impl View for WelcomePage {
                             theme::ui::checkbox::<Diagnostics, Self, _>(
                                 "Send crash reports",
                                 &theme.welcome.checkbox,
-                                diagnostics,
+                                telemetry_settings.diagnostics,
                                 0,
                                 cx,
-                                |_, checked, cx| {
-                                    SettingsFile::update(cx, move |file| {
-                                        file.telemetry.set_diagnostics(checked)
-                                    })
+                                |this, checked, cx| {
+                                    if let Some(workspace) = this.workspace.upgrade(cx) {
+                                        let fs = workspace.read(cx).app_state().fs.clone();
+                                        update_settings_file::<TelemetrySettings>(
+                                            fs,
+                                            cx,
+                                            move |setting| setting.diagnostics = Some(checked),
+                                        )
+                                    }
                                 },
                             )
                             .contained()
@@ -214,7 +222,7 @@ impl WelcomePage {
     pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
         WelcomePage {
             workspace: workspace.weak_handle(),
-            _settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
+            _settings_subscription: cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify()),
         }
     }
 }
@@ -250,7 +258,7 @@ impl Item for WelcomePage {
     ) -> Option<Self> {
         Some(WelcomePage {
             workspace: self.workspace.clone(),
-            _settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
+            _settings_subscription: cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify()),
         })
     }
 }

crates/workspace/Cargo.toml 🔗

@@ -45,6 +45,7 @@ lazy_static.workspace = true
 log.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
+schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true

crates/workspace/src/dock.rs 🔗

@@ -1,5 +1,9 @@
 mod toggle_dock_button;
 
+use crate::{
+    sidebar::SidebarSide, BackgroundActions, DockAnchor, ItemHandle, Pane, Workspace,
+    WorkspaceSettings,
+};
 use collections::HashMap;
 use gpui::{
     actions,
@@ -8,10 +12,7 @@ use gpui::{
     platform::{CursorStyle, MouseButton},
     AnyElement, AppContext, Border, Element, SizeConstraint, ViewContext, ViewHandle,
 };
-use settings::{DockAnchor, Settings};
 use theme::Theme;
-
-use crate::{sidebar::SidebarSide, BackgroundActions, ItemHandle, Pane, Workspace};
 pub use toggle_dock_button::ToggleDockButton;
 
 actions!(
@@ -171,7 +172,8 @@ impl Dock {
         background_actions: BackgroundActions,
         cx: &mut ViewContext<Workspace>,
     ) -> Self {
-        let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
+        let position =
+            DockPosition::Hidden(settings::get::<WorkspaceSettings>(cx).default_dock_anchor);
         let workspace = cx.weak_handle();
         let pane =
             cx.add_view(|cx| Pane::new(workspace, Some(position.anchor()), background_actions, cx));
@@ -405,8 +407,6 @@ mod tests {
 
     use gpui::{AppContext, BorrowWindowContext, TestAppContext, ViewContext, WindowContext};
     use project::{FakeFs, Project};
-    use settings::Settings;
-    use theme::ThemeRegistry;
 
     use super::*;
     use crate::{
@@ -417,6 +417,7 @@ mod tests {
         },
         register_deserializable_item,
         sidebar::Sidebar,
+        tests::init_test,
         AppState, ItemHandle, Workspace,
     };
 
@@ -429,8 +430,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_dock_workspace_infinite_loop(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
 
         cx.update(|cx| {
             register_deserializable_item::<item::test::TestItem>(cx);
@@ -466,7 +466,6 @@ mod tests {
                 project.clone(),
                 Arc::new(AppState {
                     languages: project.read(cx).languages().clone(),
-                    themes: ThemeRegistry::new((), cx.font_cache().clone()),
                     client: project.read(cx).client(),
                     user_store: project.read(cx).user_store(),
                     fs: project.read(cx).fs().clone(),
@@ -602,7 +601,7 @@ mod tests {
 
     impl<'a> DockTestContext<'a> {
         pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> {
-            Settings::test_async(cx);
+            init_test(cx);
             let fs = FakeFs::new(cx.background());
 
             cx.update(|cx| init(cx));
@@ -613,7 +612,6 @@ mod tests {
                     project.clone(),
                     Arc::new(AppState {
                         languages: project.read(cx).languages().clone(),
-                        themes: ThemeRegistry::new((), cx.font_cache().clone()),
                         client: project.read(cx).client(),
                         user_store: project.read(cx).user_store(),
                         fs: project.read(cx).fs().clone(),

crates/workspace/src/dock/toggle_dock_button.rs 🔗

@@ -6,7 +6,6 @@ use gpui::{
     platform::MouseButton,
     AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
 };
-use settings::Settings;
 
 pub struct ToggleDockButton {
     workspace: WeakViewHandle<Workspace>,
@@ -43,7 +42,7 @@ impl View for ToggleDockButton {
         let dock_position = workspace.read(cx).dock.position;
         let dock_pane = workspace.read(cx).dock_pane().clone();
 
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
 
         let button = MouseEventHandler::<Self, _>::new(0, cx, {
             let theme = theme.clone();

crates/workspace/src/item.rs 🔗

@@ -3,6 +3,7 @@ use crate::{
     FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace,
     WorkspaceId,
 };
+use crate::{AutosaveSetting, WorkspaceSettings};
 use anyhow::Result;
 use client::{proto, Client};
 use gpui::{
@@ -10,7 +11,6 @@ use gpui::{
     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use project::{Project, ProjectEntryId, ProjectPath};
-use settings::{Autosave, Settings};
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -450,8 +450,11 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                             }
 
                             ItemEvent::Edit => {
-                                if let Autosave::AfterDelay { milliseconds } =
-                                    cx.global::<Settings>().autosave
+                                let settings = settings::get::<WorkspaceSettings>(cx);
+                                let debounce_delay = settings.git.gutter_debounce;
+
+                                if let AutosaveSetting::AfterDelay { milliseconds } =
+                                    settings.autosave
                                 {
                                     let delay = Duration::from_millis(milliseconds);
                                     let item = item.clone();
@@ -460,9 +463,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                                     });
                                 }
 
-                                let settings = cx.global::<Settings>();
-                                let debounce_delay = settings.git_overrides.gutter_debounce;
-
                                 let item = item.clone();
 
                                 if let Some(delay) = debounce_delay {
@@ -500,7 +500,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
                 }));
 
             cx.observe_focus(self, move |workspace, item, focused, cx| {
-                if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
+                if !focused
+                    && settings::get::<WorkspaceSettings>(cx).autosave
+                        == AutosaveSetting::OnFocusChange
+                {
                     Pane::autosave_item(&item, workspace.project.clone(), cx)
                         .detach_and_log_err(cx);
                 }

crates/workspace/src/notifications.rs 🔗

@@ -149,6 +149,8 @@ impl Workspace {
 }
 
 pub mod simple_message_notification {
+    use super::Notification;
+    use crate::Workspace;
     use gpui::{
         actions,
         elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
@@ -158,13 +160,8 @@ pub mod simple_message_notification {
     };
     use menu::Cancel;
     use serde::Deserialize;
-    use settings::Settings;
     use std::{borrow::Cow, sync::Arc};
 
-    use crate::Workspace;
-
-    use super::Notification;
-
     actions!(message_notifications, [CancelMessageNotification]);
 
     #[derive(Clone, Default, Deserialize, PartialEq)]
@@ -240,7 +237,7 @@ pub mod simple_message_notification {
         }
 
         fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
-            let theme = cx.global::<Settings>().theme.clone();
+            let theme = theme::current(cx).clone();
             let theme = &theme.simple_message_notification;
 
             enum MessageNotificationTag {}

crates/workspace/src/pane.rs 🔗

@@ -5,7 +5,8 @@ use crate::{
     dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, Dock, ExpandDock},
     item::WeakItemHandle,
     toolbar::Toolbar,
-    Item, NewFile, NewSearch, NewTerminal, Workspace,
+    AutosaveSetting, DockAnchor, Item, NewFile, NewSearch, NewTerminal, Workspace,
+    WorkspaceSettings,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
@@ -29,7 +30,6 @@ use gpui::{
 };
 use project::{Project, ProjectEntryId, ProjectPath};
 use serde::Deserialize;
-use settings::{Autosave, DockAnchor, Settings};
 use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
 use theme::Theme;
 use util::ResultExt;
@@ -1024,8 +1024,8 @@ impl Pane {
         } else if is_dirty && (can_save || is_singleton) {
             let will_autosave = cx.read(|cx| {
                 matches!(
-                    cx.global::<Settings>().autosave,
-                    Autosave::OnFocusChange | Autosave::OnWindowChange
+                    settings::get::<WorkspaceSettings>(cx).autosave,
+                    AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
                 ) && Self::can_autosave_item(&*item, cx)
             });
             let should_save = if should_prompt_for_save && !will_autosave {
@@ -1296,7 +1296,7 @@ impl Pane {
     }
 
     fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
 
         let pane = cx.handle().downgrade();
         let autoscroll = if mem::take(&mut self.autoscroll) {
@@ -1327,7 +1327,7 @@ impl Pane {
                         let pane = pane.clone();
                         let detail = detail.clone();
 
-                        let theme = cx.global::<Settings>().theme.clone();
+                        let theme = theme::current(cx).clone();
                         let mut tooltip_theme = theme.tooltip.clone();
                         tooltip_theme.max_text_width = None;
                         let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string());
@@ -1405,7 +1405,7 @@ impl Pane {
                         pane: pane.clone(),
                     },
                     {
-                        let theme = cx.global::<Settings>().theme.clone();
+                        let theme = theme::current(cx).clone();
 
                         let detail = detail.clone();
                         move |dragged_item: &DraggedItem, cx: &mut ViewContext<Workspace>| {
@@ -1698,7 +1698,7 @@ impl View for Pane {
             if let Some(active_item) = self.active_item() {
                 Flex::column()
                     .with_child({
-                        let theme = cx.global::<Settings>().theme.clone();
+                        let theme = theme::current(cx).clone();
 
                         let mut stack = Stack::new();
 
@@ -1764,7 +1764,7 @@ impl View for Pane {
                     .into_any()
             } else {
                 enum EmptyPane {}
-                let theme = cx.global::<Settings>().theme.clone();
+                let theme = theme::current(cx).clone();
 
                 dragged_item_receiver::<EmptyPane, _, _>(0, 0, false, None, cx, |_, cx| {
                     self.render_blank_pane(&theme, cx)
@@ -1861,7 +1861,7 @@ fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
     Stack::new()
         .with_child(
             MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
-                let theme = &cx.global::<Settings>().theme.workspace.tab_bar;
+                let theme = &theme::current(cx).workspace.tab_bar;
                 let style = theme.pane_button.style_for(mouse_state, false);
                 Svg::new(icon)
                     .with_color(style.color)
@@ -2023,7 +2023,7 @@ impl<V: View> Element<V> for PaneBackdrop<V> {
         view: &mut V,
         cx: &mut ViewContext<V>,
     ) -> Self::PaintState {
-        let background = cx.global::<Settings>().theme.editor.background;
+        let background = theme::current(cx).editor.background;
 
         let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
 
@@ -2087,10 +2087,11 @@ mod tests {
     use crate::item::test::{TestItem, TestProjectItem};
     use gpui::{executor::Deterministic, TestAppContext};
     use project::FakeFs;
+    use settings::SettingsStore;
 
     #[gpui::test]
     async fn test_remove_active_empty(cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2104,7 +2105,7 @@ mod tests {
     #[gpui::test]
     async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2192,7 +2193,7 @@ mod tests {
     #[gpui::test]
     async fn test_add_item_with_existing_item(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2268,7 +2269,7 @@ mod tests {
     #[gpui::test]
     async fn test_add_item_with_same_project_entries(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2377,7 +2378,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_remove_item_ordering(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2424,7 +2425,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_close_inactive_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2443,7 +2444,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_close_clean_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2470,7 +2471,7 @@ mod tests {
         deterministic: Arc<Deterministic>,
         cx: &mut TestAppContext,
     ) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2492,7 +2493,7 @@ mod tests {
         deterministic: Arc<Deterministic>,
         cx: &mut TestAppContext,
     ) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2511,7 +2512,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_close_all_items(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-        Settings::test_async(cx);
+        init_test(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -2531,6 +2532,14 @@ mod tests {
         assert_item_labels(&pane, [], cx);
     }
 
+    fn init_test(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            crate::init_settings(cx);
+        });
+    }
+
     fn add_labeled_item(
         workspace: &ViewHandle<Workspace>,
         pane: &ViewHandle<Pane>,

crates/workspace/src/pane/dragged_item_receiver.rs 🔗

@@ -1,3 +1,5 @@
+use super::DraggedItem;
+use crate::{Pane, SplitDirection, Workspace};
 use drag_and_drop::DragAndDrop;
 use gpui::{
     color::Color,
@@ -8,11 +10,6 @@ use gpui::{
     AppContext, Element, EventContext, MouseState, Quad, View, ViewContext, WeakViewHandle,
 };
 use project::ProjectEntryId;
-use settings::Settings;
-
-use crate::{Pane, SplitDirection, Workspace};
-
-use super::DraggedItem;
 
 pub fn dragged_item_receiver<Tag, D, F>(
     region_id: usize,
@@ -225,8 +222,5 @@ fn drop_split_direction(
 }
 
 fn overlay_color(cx: &AppContext) -> Color {
-    cx.global::<Settings>()
-        .theme
-        .workspace
-        .drop_target_overlay_color
+    theme::current(cx).workspace.drop_target_overlay_color
 }

crates/workspace/src/pane_group.rs 🔗

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use crate::{AppState, FollowerStatesByLeader, Pane, Workspace};
+use crate::{AppState, FollowerStatesByLeader, Pane, Workspace, WorkspaceSettings};
 use anyhow::{anyhow, Result};
 use call::{ActiveCall, ParticipantLocation};
 use gpui::{
@@ -11,7 +11,6 @@ use gpui::{
 };
 use project::Project;
 use serde::Deserialize;
-use settings::Settings;
 use theme::Theme;
 
 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -380,7 +379,7 @@ impl PaneAxis {
             .with_children(self.members.iter().enumerate().map(|(ix, member)| {
                 let mut flex = 1.0;
                 if member.contains(active_pane) {
-                    flex = cx.global::<Settings>().active_pane_magnification;
+                    flex = settings::get::<WorkspaceSettings>(cx).active_pane_magnification;
                 }
 
                 let mut member = member.render(

crates/workspace/src/persistence.rs 🔗

@@ -497,13 +497,10 @@ impl WorkspaceDb {
 
 #[cfg(test)]
 mod tests {
-
-    use std::sync::Arc;
-
-    use db::open_test_db;
-    use settings::DockAnchor;
-
     use super::*;
+    use crate::DockAnchor;
+    use db::open_test_db;
+    use std::sync::Arc;
 
     #[gpui::test]
     async fn test_next_id_stability() {

crates/workspace/src/persistence/model.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    dock::DockPosition, item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace,
-    WorkspaceId,
+    dock::DockPosition, item::ItemHandle, DockAnchor, ItemDeserializers, Member, Pane, PaneAxis,
+    Workspace, WorkspaceId,
 };
 use anyhow::{anyhow, Context, Result};
 use async_recursion::async_recursion;
@@ -12,7 +12,6 @@ use gpui::{
     platform::WindowBounds, AsyncAppContext, Axis, ModelHandle, Task, ViewHandle, WeakViewHandle,
 };
 use project::Project;
-use settings::DockAnchor;
 use std::{
     path::{Path, PathBuf},
     sync::Arc,
@@ -316,10 +315,9 @@ impl Column for DockPosition {
 
 #[cfg(test)]
 mod tests {
-    use db::sqlez::connection::Connection;
-    use settings::DockAnchor;
-
     use super::WorkspaceLocation;
+    use crate::DockAnchor;
+    use db::sqlez::connection::Connection;
 
     #[test]
     fn test_workspace_round_trips() {

crates/workspace/src/shared_screen.rs 🔗

@@ -12,7 +12,6 @@ use gpui::{
     platform::MouseButton,
     AppContext, Entity, Task, View, ViewContext,
 };
-use settings::Settings;
 use smallvec::SmallVec;
 use std::{
     borrow::Cow,
@@ -88,7 +87,7 @@ impl View for SharedScreen {
                 }
             })
             .contained()
-            .with_style(cx.global::<Settings>().theme.shared_screen)
+            .with_style(theme::current(cx).shared_screen)
         })
         .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent())
         .into_any()

crates/workspace/src/sidebar.rs 🔗

@@ -4,7 +4,6 @@ use gpui::{
     AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use serde::Deserialize;
-use settings::Settings;
 use std::rc::Rc;
 
 pub trait SidebarItem: View {
@@ -192,7 +191,7 @@ impl View for Sidebar {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
         if let Some(active_item) = self.active_item() {
             enum ResizeHandleTag {}
-            let style = &cx.global::<Settings>().theme.workspace.sidebar;
+            let style = &theme::current(cx).workspace.sidebar;
             ChildView::new(active_item.as_any(), cx)
                 .contained()
                 .with_style(style.container)
@@ -231,7 +230,7 @@ impl View for SidebarButtons {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &cx.global::<Settings>().theme;
+        let theme = &theme::current(cx);
         let tooltip_style = theme.tooltip.clone();
         let theme = &theme.workspace.status_bar.sidebar_buttons;
         let sidebar = self.sidebar.read(cx);

crates/workspace/src/status_bar.rs 🔗

@@ -11,7 +11,6 @@ use gpui::{
     AnyElement, AnyViewHandle, Entity, LayoutContext, SceneBuilder, SizeConstraint, Subscription,
     View, ViewContext, ViewHandle, WindowContext,
 };
-use settings::Settings;
 
 pub trait StatusItemView: View {
     fn set_active_pane_item(
@@ -47,7 +46,7 @@ impl View for StatusBar {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &cx.global::<Settings>().theme.workspace.status_bar;
+        let theme = &theme::current(cx).workspace.status_bar;
 
         StatusBarElement {
             left: Flex::row()

crates/workspace/src/toolbar.rs 🔗

@@ -3,7 +3,6 @@ use gpui::{
     elements::*, platform::CursorStyle, platform::MouseButton, Action, AnyElement, AnyViewHandle,
     AppContext, Entity, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
-use settings::Settings;
 
 pub trait ToolbarItemView: View {
     fn set_active_pane_item(
@@ -68,7 +67,7 @@ impl View for Toolbar {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = &cx.global::<Settings>().theme.workspace.toolbar;
+        let theme = &theme::current(cx).workspace.toolbar;
 
         let mut primary_left_items = Vec::new();
         let mut primary_right_items = Vec::new();
@@ -131,7 +130,7 @@ impl View for Toolbar {
         let height = theme.height * primary_items_row_count as f32;
         let nav_button_height = theme.height;
         let button_style = theme.nav_button;
-        let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+        let tooltip_style = theme::current(cx).tooltip.clone();
 
         Flex::column()
             .with_child(

crates/workspace/src/workspace.rs 🔗

@@ -13,6 +13,7 @@ pub mod shared_screen;
 pub mod sidebar;
 mod status_bar;
 mod toolbar;
+mod workspace_settings;
 
 use anyhow::{anyhow, Context, Result};
 use assets::Assets;
@@ -75,14 +76,14 @@ pub use persistence::{
 use postage::prelude::Stream;
 use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use serde::Deserialize;
-use settings::{Autosave, DockAnchor, Settings};
 use shared_screen::SharedScreen;
 use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem};
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
-use theme::{Theme, ThemeRegistry};
+use theme::Theme;
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
 use util::{async_iife, paths, ResultExt};
+pub use workspace_settings::{AutosaveSetting, DockAnchor, GitGutterSetting, WorkspaceSettings};
 
 lazy_static! {
     static ref ZED_WINDOW_SIZE: Option<Vector2F> = env::var("ZED_WINDOW_SIZE")
@@ -183,7 +184,12 @@ pub type WorkspaceId = i64;
 
 impl_actions!(workspace, [ActivatePane]);
 
+pub fn init_settings(cx: &mut AppContext) {
+    settings::register::<WorkspaceSettings>(cx);
+}
+
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
+    init_settings(cx);
     pane::init(cx);
     dock::init(cx);
     notifications::init(cx);
@@ -269,7 +275,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     cx.add_action(
         move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
             create_and_open_local_file(&paths::SETTINGS, cx, || {
-                Settings::initial_user_settings_content(&Assets)
+                settings::initial_user_settings_content(&Assets)
                     .as_ref()
                     .into()
             })
@@ -354,7 +360,6 @@ pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
 
 pub struct AppState {
     pub languages: Arc<LanguageRegistry>,
-    pub themes: Arc<ThemeRegistry>,
     pub client: Arc<client::Client>,
     pub user_store: ModelHandle<client::UserStore>,
     pub fs: Arc<dyn fs::Fs>,
@@ -368,18 +373,24 @@ pub struct AppState {
 impl AppState {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut AppContext) -> Arc<Self> {
-        let settings = Settings::test(cx);
-        cx.set_global(settings);
+        use settings::SettingsStore;
+
+        if !cx.has_global::<SettingsStore>() {
+            cx.set_global(SettingsStore::test(cx));
+        }
 
         let fs = fs::FakeFs::new(cx.background().clone());
         let languages = Arc::new(LanguageRegistry::test());
         let http_client = util::http::FakeHttpClient::with_404_response();
         let client = Client::new(http_client.clone(), cx);
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
-        let themes = ThemeRegistry::new((), cx.font_cache().clone());
+
+        theme::init((), cx);
+        client::init(&client, cx);
+        crate::init_settings(cx);
+
         Arc::new(Self {
             client,
-            themes,
             fs,
             languages,
             user_store,
@@ -1977,7 +1988,7 @@ impl Workspace {
             enum DisconnectedOverlay {}
             Some(
                 MouseEventHandler::<DisconnectedOverlay, _>::new(0, cx, |_, cx| {
-                    let theme = &cx.global::<Settings>().theme;
+                    let theme = &theme::current(cx);
                     Label::new(
                         "Your connection to the remote project has been lost.",
                         theme.workspace.disconnected_overlay.text.clone(),
@@ -2348,8 +2359,8 @@ impl Workspace {
                         item.workspace_deactivated(cx);
                     }
                     if matches!(
-                        cx.global::<Settings>().autosave,
-                        Autosave::OnWindowChange | Autosave::OnFocusChange
+                        settings::get::<WorkspaceSettings>(cx).autosave,
+                        AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
                     ) {
                         for item in pane.items() {
                             Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
@@ -2615,7 +2626,6 @@ impl Workspace {
     pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
         let app_state = Arc::new(AppState {
             languages: project.read(cx).languages().clone(),
-            themes: ThemeRegistry::new((), cx.font_cache().clone()),
             client: project.read(cx).client(),
             user_store: project.read(cx).user_store(),
             fs: project.read(cx).fs().clone(),
@@ -2761,7 +2771,7 @@ impl View for Workspace {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let theme = cx.global::<Settings>().theme.clone();
+        let theme = theme::current(cx).clone();
         Stack::new()
             .with_child(
                 Flex::column()
@@ -3130,7 +3140,7 @@ pub fn join_remote_project(
 }
 
 pub fn restart(_: &Restart, cx: &mut AppContext) {
-    let should_confirm = cx.global::<Settings>().confirm_quit;
+    let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
     cx.spawn(|mut cx| async move {
         let mut workspaces = cx
             .window_ids()
@@ -3191,20 +3201,18 @@ fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
 
 #[cfg(test)]
 mod tests {
-    use std::{cell::RefCell, rc::Rc};
-
-    use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
-
     use super::*;
+    use crate::item::test::{TestItem, TestItemEvent, TestProjectItem};
     use fs::FakeFs;
     use gpui::{executor::Deterministic, TestAppContext};
     use project::{Project, ProjectEntryId};
     use serde_json::json;
+    use settings::SettingsStore;
+    use std::{cell::RefCell, rc::Rc};
 
     #[gpui::test]
     async fn test_tab_disambiguation(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
 
         let fs = FakeFs::new(cx.background());
         let project = Project::test(fs, [], cx).await;
@@ -3252,8 +3260,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_tracking_active_path(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
             "/root1",
@@ -3356,8 +3364,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_close_window(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/root", json!({ "one": "" })).await;
 
@@ -3392,8 +3400,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_close_pane_items(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
@@ -3499,8 +3507,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
-        Settings::test_async(cx);
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
@@ -3605,9 +3613,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
-        deterministic.forbid_parking();
+        init_test(cx);
 
-        Settings::test_async(cx);
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
@@ -3623,8 +3630,10 @@ mod tests {
 
         // Autosave on window change.
         item.update(cx, |item, cx| {
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.autosave = Autosave::OnWindowChange;
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
+                })
             });
             item.is_dirty = true;
         });
@@ -3637,8 +3646,10 @@ mod tests {
         // Autosave on focus change.
         item.update(cx, |item, cx| {
             cx.focus_self();
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.autosave = Autosave::OnFocusChange;
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
+                })
             });
             item.is_dirty = true;
         });
@@ -3661,8 +3672,10 @@ mod tests {
 
         // Autosave after delay.
         item.update(cx, |item, cx| {
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
+                })
             });
             item.is_dirty = true;
             cx.emit(TestItemEvent::Edit);
@@ -3678,8 +3691,10 @@ mod tests {
 
         // Autosave on focus change, ensuring closing the tab counts as such.
         item.update(cx, |item, cx| {
-            cx.update_global(|settings: &mut Settings, _| {
-                settings.autosave = Autosave::OnFocusChange;
+            cx.update_global(|settings: &mut SettingsStore, cx| {
+                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
+                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
+                })
             });
             item.is_dirty = true;
         });
@@ -3719,12 +3734,9 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_pane_navigation(
-        deterministic: Arc<Deterministic>,
-        cx: &mut gpui::TestAppContext,
-    ) {
-        deterministic.forbid_parking();
-        Settings::test_async(cx);
+    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, [], cx).await;
@@ -3776,4 +3788,14 @@ mod tests {
             assert!(pane.can_navigate_forward());
         });
     }
+
+    pub fn init_test(cx: &mut TestAppContext) {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init((), cx);
+            language::init(cx);
+            crate::init_settings(cx);
+        });
+    }
 }

crates/workspace/src/workspace_settings.rs 🔗

@@ -0,0 +1,103 @@
+use anyhow::bail;
+use db::sqlez::{
+    bindable::{Bind, Column, StaticColumnCount},
+    statement::Statement,
+};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Deserialize)]
+pub struct WorkspaceSettings {
+    pub active_pane_magnification: f32,
+    pub confirm_quit: bool,
+    pub show_call_status_icon: bool,
+    pub autosave: AutosaveSetting,
+    pub default_dock_anchor: DockAnchor,
+    pub git: GitSettings,
+}
+
+#[derive(Clone, Serialize, Deserialize, JsonSchema)]
+pub struct WorkspaceSettingsContent {
+    pub active_pane_magnification: Option<f32>,
+    pub confirm_quit: Option<bool>,
+    pub show_call_status_icon: Option<bool>,
+    pub autosave: Option<AutosaveSetting>,
+    pub default_dock_anchor: Option<DockAnchor>,
+    pub git: Option<GitSettings>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum AutosaveSetting {
+    Off,
+    AfterDelay { milliseconds: u64 },
+    OnFocusChange,
+    OnWindowChange,
+}
+
+#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DockAnchor {
+    #[default]
+    Bottom,
+    Right,
+    Expanded,
+}
+
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
+pub struct GitSettings {
+    pub git_gutter: Option<GitGutterSetting>,
+    pub gutter_debounce: Option<u64>,
+}
+
+#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum GitGutterSetting {
+    #[default]
+    TrackedFiles,
+    Hide,
+}
+
+impl StaticColumnCount for DockAnchor {}
+
+impl Bind for DockAnchor {
+    fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
+        match self {
+            DockAnchor::Bottom => "Bottom",
+            DockAnchor::Right => "Right",
+            DockAnchor::Expanded => "Expanded",
+        }
+        .bind(statement, start_index)
+    }
+}
+
+impl Column for DockAnchor {
+    fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
+        String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
+            Ok((
+                match anchor_text.as_ref() {
+                    "Bottom" => DockAnchor::Bottom,
+                    "Right" => DockAnchor::Right,
+                    "Expanded" => DockAnchor::Expanded,
+                    _ => bail!("Stored dock anchor is incorrect"),
+                },
+                next_index,
+            ))
+        })
+    }
+}
+
+impl Setting for WorkspaceSettings {
+    const KEY: Option<&'static str> = None;
+
+    type FileContent = WorkspaceSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/zed/Cargo.toml 🔗

@@ -101,7 +101,7 @@ smol.workspace = true
 tempdir.workspace = true
 thiserror.workspace = true
 tiny_http = "0.8"
-toml = "0.5"
+toml.workspace = true
 tree-sitter = "0.20"
 tree-sitter-c = "0.20.1"
 tree-sitter-cpp = "0.20.0"

crates/zed/src/languages.rs 🔗

@@ -3,7 +3,6 @@ pub use language::*;
 use node_runtime::NodeRuntime;
 use rust_embed::RustEmbed;
 use std::{borrow::Cow, str, sync::Arc};
-use theme::ThemeRegistry;
 
 mod c;
 mod elixir;
@@ -32,11 +31,7 @@ mod yaml;
 #[exclude = "*.rs"]
 struct LanguageDir;
 
-pub fn init(
-    languages: Arc<LanguageRegistry>,
-    themes: Arc<ThemeRegistry>,
-    node_runtime: Arc<NodeRuntime>,
-) {
+pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
     fn adapter_arc(adapter: impl LspAdapter) -> Arc<dyn LspAdapter> {
         Arc::new(adapter)
     }
@@ -69,7 +64,6 @@ pub fn init(
             vec![adapter_arc(json::JsonLspAdapter::new(
                 node_runtime.clone(),
                 languages.clone(),
-                themes.clone(),
             ))],
         ),
         ("markdown", tree_sitter_markdown::language(), vec![]),

crates/zed/src/languages/c.rs 🔗

@@ -249,16 +249,21 @@ impl super::LspAdapter for CLspAdapter {
 #[cfg(test)]
 mod tests {
     use gpui::TestAppContext;
-    use language::{AutoindentMode, Buffer};
-    use settings::Settings;
+    use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
+    use settings::SettingsStore;
+    use std::num::NonZeroU32;
 
     #[gpui::test]
     async fn test_c_autoindent(cx: &mut TestAppContext) {
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
         cx.update(|cx| {
-            let mut settings = Settings::test(cx);
-            settings.editor_overrides.tab_size = Some(2.try_into().unwrap());
-            cx.set_global(settings);
+            cx.set_global(SettingsStore::test(cx));
+            language::init(cx);
+            cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                    s.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
         });
         let language = crate::languages::language("c", tree_sitter_c::language(), None).await;
 

crates/zed/src/languages/json.rs 🔗

@@ -6,7 +6,7 @@ use gpui::AppContext;
 use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
 use node_runtime::NodeRuntime;
 use serde_json::json;
-use settings::{keymap_file_json_schema, settings_file_json_schema};
+use settings::{keymap_file_json_schema, SettingsJsonSchemaParams, SettingsStore};
 use smol::fs;
 use staff_mode::StaffMode;
 use std::{
@@ -16,7 +16,6 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use theme::ThemeRegistry;
 use util::http::HttpClient;
 use util::{paths, ResultExt};
 
@@ -30,20 +29,11 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
 pub struct JsonLspAdapter {
     node: Arc<NodeRuntime>,
     languages: Arc<LanguageRegistry>,
-    themes: Arc<ThemeRegistry>,
 }
 
 impl JsonLspAdapter {
-    pub fn new(
-        node: Arc<NodeRuntime>,
-        languages: Arc<LanguageRegistry>,
-        themes: Arc<ThemeRegistry>,
-    ) -> Self {
-        JsonLspAdapter {
-            node,
-            languages,
-            themes,
-        }
+    pub fn new(node: Arc<NodeRuntime>, languages: Arc<LanguageRegistry>) -> Self {
+        JsonLspAdapter { node, languages }
     }
 }
 
@@ -128,12 +118,15 @@ impl LspAdapter for JsonLspAdapter {
         cx: &mut AppContext,
     ) -> Option<BoxFuture<'static, serde_json::Value>> {
         let action_names = cx.all_action_names().collect::<Vec<_>>();
-        let theme_names = self
-            .themes
-            .list(**cx.default_global::<StaffMode>())
-            .map(|meta| meta.name)
-            .collect();
-        let language_names = self.languages.language_names();
+        let staff_mode = cx.global::<StaffMode>().0;
+        let language_names = &self.languages.language_names();
+        let settings_schema = cx.global::<SettingsStore>().json_schema(
+            &SettingsJsonSchemaParams {
+                language_names,
+                staff_mode,
+            },
+            cx,
+        );
         Some(
             future::ready(serde_json::json!({
                 "json": {
@@ -143,7 +136,7 @@ impl LspAdapter for JsonLspAdapter {
                     "schemas": [
                         {
                             "fileMatch": [schema_file_match(&paths::SETTINGS)],
-                            "schema": settings_file_json_schema(theme_names, &language_names),
+                            "schema": settings_schema,
                         },
                         {
                             "fileMatch": [schema_file_match(&paths::KEYMAP)],

crates/zed/src/languages/python.rs 🔗

@@ -170,8 +170,9 @@ impl LspAdapter for PythonLspAdapter {
 #[cfg(test)]
 mod tests {
     use gpui::{ModelContext, TestAppContext};
-    use language::{AutoindentMode, Buffer};
-    use settings::Settings;
+    use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer};
+    use settings::SettingsStore;
+    use std::num::NonZeroU32;
 
     #[gpui::test]
     async fn test_python_autoindent(cx: &mut TestAppContext) {
@@ -179,9 +180,13 @@ mod tests {
         let language =
             crate::languages::language("python", tree_sitter_python::language(), None).await;
         cx.update(|cx| {
-            let mut settings = Settings::test(cx);
-            settings.editor_overrides.tab_size = Some(2.try_into().unwrap());
-            cx.set_global(settings);
+            cx.set_global(SettingsStore::test(cx));
+            language::init(cx);
+            cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                    s.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
         });
 
         cx.add_model(|cx| {

crates/zed/src/languages/rust.rs 🔗

@@ -253,10 +253,13 @@ impl LspAdapter for RustLspAdapter {
 
 #[cfg(test)]
 mod tests {
+    use std::num::NonZeroU32;
+
     use super::*;
     use crate::languages::language;
     use gpui::{color::Color, TestAppContext};
-    use settings::Settings;
+    use language::language_settings::AllLanguageSettings;
+    use settings::SettingsStore;
     use theme::SyntaxTheme;
 
     #[gpui::test]
@@ -435,9 +438,13 @@ mod tests {
     async fn test_rust_autoindent(cx: &mut TestAppContext) {
         cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
         cx.update(|cx| {
-            let mut settings = Settings::test(cx);
-            settings.editor_overrides.tab_size = Some(2.try_into().unwrap());
-            cx.set_global(settings);
+            cx.set_global(SettingsStore::test(cx));
+            language::init(cx);
+            cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+                    s.defaults.tab_size = NonZeroU32::new(2);
+                });
+            });
         });
 
         let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await;

crates/zed/src/languages/yaml.rs 🔗

@@ -2,10 +2,11 @@ use anyhow::{anyhow, Result};
 use async_trait::async_trait;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
-use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
+use language::{
+    language_settings::language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
+};
 use node_runtime::NodeRuntime;
 use serde_json::Value;
-use settings::Settings;
 use smol::fs;
 use std::{
     any::Any,
@@ -100,14 +101,13 @@ impl LspAdapter for YamlLspAdapter {
     }
 
     fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
-        let settings = cx.global::<Settings>();
         Some(
             future::ready(serde_json::json!({
                 "yaml": {
                     "keyOrdering": false
                 },
                 "[yaml]": {
-                    "editor.tabSize": settings.tab_size(Some("YAML"))
+                    "editor.tabSize": language_settings(Some("YAML"), cx).tab_size,
                 }
             }))
             .boxed(),

crates/zed/src/main.rs 🔗

@@ -8,7 +8,7 @@ use cli::{
     ipc::{self, IpcSender},
     CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
 };
-use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
+use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
 use db::kvp::KEY_VALUE_STORE;
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use futures::{
@@ -17,16 +17,13 @@ use futures::{
 };
 use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task, ViewContext};
 use isahc::{config::Configurable, Request};
-use language::LanguageRegistry;
+use language::{LanguageRegistry, Point};
 use log::LevelFilter;
 use node_runtime::NodeRuntime;
 use parking_lot::Mutex;
 use project::Fs;
 use serde::{Deserialize, Serialize};
-use settings::{
-    self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent,
-    WorkingDirectory,
-};
+use settings::{default_settings, handle_settings_file_changes, watch_config_file, SettingsStore};
 use simplelog::ConfigBuilder;
 use smol::process::Command;
 use std::{
@@ -38,6 +35,7 @@ use std::{
     os::unix::prelude::OsStrExt,
     panic,
     path::{Path, PathBuf},
+    str,
     sync::{
         atomic::{AtomicBool, Ordering},
         Arc, Weak,
@@ -46,8 +44,7 @@ use std::{
     time::Duration,
 };
 use sum_tree::Bias;
-use terminal_view::{get_working_directory, TerminalView};
-use text::Point;
+use terminal_view::{get_working_directory, TerminalSettings, TerminalView};
 use util::{
     http::{self, HttpClient},
     paths::PathLikeWithPosition,
@@ -55,16 +52,16 @@ use util::{
 use welcome::{show_welcome_experience, FIRST_OPEN};
 
 use fs::RealFs;
-use settings::watched_json::WatchedJsonFile;
 #[cfg(debug_assertions)]
 use staff_mode::StaffMode;
-use theme::ThemeRegistry;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
 use workspace::{
     dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings,
     Workspace,
 };
-use zed::{self, build_window_options, initialize_workspace, languages, menus};
+use zed::{
+    self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
+};
 
 fn main() {
     let http = http::client();
@@ -84,10 +81,10 @@ fn main() {
     load_embedded_fonts(&app);
 
     let fs = Arc::new(RealFs);
-
-    let themes = ThemeRegistry::new(Assets, app.font_cache());
-    let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes);
-    let config_files = load_config_files(&app, fs.clone());
+    let user_settings_file_rx =
+        watch_config_file(app.background(), fs.clone(), paths::SETTINGS.clone());
+    let user_keymap_file_rx =
+        watch_config_file(app.background(), fs.clone(), paths::KEYMAP.clone());
 
     let login_shell_env_loaded = if stdout_is_a_pty() {
         Task::ready(())
@@ -127,22 +124,13 @@ fn main() {
         #[cfg(debug_assertions)]
         cx.set_global(StaffMode(true));
 
-        let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap();
-
-        //Setup settings global before binding actions
-        cx.set_global(SettingsFile::new(
-            &paths::SETTINGS,
-            settings_file_content.clone(),
-            fs.clone(),
-        ));
-
-        settings::watch_files(
-            default_settings,
-            settings_file_content,
-            themes.clone(),
-            keymap_file,
-            cx,
-        );
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+        handle_settings_file_changes(user_settings_file_rx, cx);
+        handle_keymap_file_changes(user_keymap_file_rx, cx);
 
         if !stdout_is_a_pty() {
             upload_previous_panics(http.clone(), cx);
@@ -155,15 +143,17 @@ fn main() {
         let languages = Arc::new(languages);
         let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned());
 
-        languages::init(languages.clone(), themes.clone(), node_runtime.clone());
+        languages::init(languages.clone(), node_runtime.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
 
         cx.set_global(client.clone());
 
+        theme::init(Assets, cx);
         context_menu::init(cx);
-        project::Project::init(&client);
-        client::init(client.clone(), cx);
+        project::Project::init(&client, cx);
+        client::init(&client, cx);
         command_palette::init(cx);
+        language::init(cx);
         editor::init(cx);
         go_to_line::init(cx);
         file_finder::init(cx);
@@ -177,13 +167,12 @@ fn main() {
         theme_testbench::init(cx);
         copilot::init(http.clone(), node_runtime, cx);
 
-        cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
-            .detach();
+        cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
 
-        languages.set_theme(cx.global::<Settings>().theme.clone());
-        cx.observe_global::<Settings, _>({
+        languages.set_theme(theme::current(cx).clone());
+        cx.observe_global::<SettingsStore, _>({
             let languages = languages.clone();
-            move |cx| languages.set_theme(cx.global::<Settings>().theme.clone())
+            move |cx| languages.set_theme(theme::current(cx).clone())
         })
         .detach();
 
@@ -191,12 +180,11 @@ fn main() {
         client.telemetry().report_mixpanel_event(
             "start app",
             Default::default(),
-            cx.global::<Settings>().telemetry(),
+            *settings::get::<TelemetrySettings>(cx),
         );
 
         let app_state = Arc::new(AppState {
             languages,
-            themes,
             client: client.clone(),
             user_store,
             fs,
@@ -214,10 +202,13 @@ fn main() {
         journal::init(app_state.clone(), cx);
         language_selector::init(cx);
         theme_selector::init(cx);
-        zed::init(&app_state, cx);
+        activity_indicator::init(cx);
+        lsp_log::init(cx);
+        call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
         collab_ui::init(&app_state, cx);
         feedback::init(cx);
         welcome::init(cx);
+        zed::init(&app_state, cx);
 
         cx.set_menus(menus::menus());
 
@@ -450,7 +441,7 @@ fn init_panic_hook(app_version: String) {
 }
 
 fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
-    let diagnostics_telemetry = cx.global::<Settings>().telemetry_diagnostics();
+    let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
 
     cx.background()
         .spawn({
@@ -480,7 +471,7 @@ fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
                         continue;
                     };
 
-                    if diagnostics_telemetry {
+                    if telemetry_settings.diagnostics {
                         let panic_data_text = smol::fs::read_to_string(&child_path)
                             .await
                             .context("error reading panic file")?;
@@ -590,11 +581,7 @@ fn load_embedded_fonts(app: &App) {
 }
 
 #[cfg(debug_assertions)]
-async fn watch_themes(
-    fs: Arc<dyn Fs>,
-    themes: Arc<ThemeRegistry>,
-    mut cx: AsyncAppContext,
-) -> Option<()> {
+async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {
     let mut events = fs
         .watch("styles/src".as_ref(), Duration::from_millis(100))
         .await;
@@ -606,7 +593,7 @@ async fn watch_themes(
             .await
             .log_err()?;
         if output.status.success() {
-            cx.update(|cx| theme_selector::reload(themes.clone(), cx))
+            cx.update(|cx| theme_selector::reload(cx))
         } else {
             eprintln!(
                 "build script failed {}",
@@ -626,27 +613,6 @@ async fn watch_themes(
     None
 }
 
-fn load_config_files(
-    app: &App,
-    fs: Arc<dyn Fs>,
-) -> oneshot::Receiver<(
-    WatchedJsonFile<SettingsFileContent>,
-    WatchedJsonFile<KeymapFileContent>,
-)> {
-    let executor = app.background();
-    let (tx, rx) = oneshot::channel();
-    executor
-        .clone()
-        .spawn(async move {
-            let settings_file =
-                WatchedJsonFile::new(fs.clone(), &executor, paths::SETTINGS.clone()).await;
-            let keymap_file = WatchedJsonFile::new(fs, &executor, paths::KEYMAP.clone()).await;
-            tx.send((settings_file, keymap_file)).ok()
-        })
-        .detach();
-    rx
-}
-
 fn connect_to_cli(
     server_name: &str,
 ) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
@@ -834,13 +800,9 @@ pub fn dock_default_item_factory(
     workspace: &mut Workspace,
     cx: &mut ViewContext<Workspace>,
 ) -> Option<Box<dyn ItemHandle>> {
-    let strategy = cx
-        .global::<Settings>()
-        .terminal_overrides
+    let strategy = settings::get::<TerminalSettings>(cx)
         .working_directory
-        .clone()
-        .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
-
+        .clone();
     let working_directory = get_working_directory(workspace, cx, strategy);
 
     let window_id = cx.window_id();

crates/zed/src/zed.rs 🔗

@@ -15,7 +15,7 @@ use anyhow::anyhow;
 use feedback::{
     feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton,
 };
-use futures::StreamExt;
+use futures::{channel::mpsc, StreamExt};
 use gpui::{
     actions,
     geometry::vector::vec2f,
@@ -29,15 +29,16 @@ use project_panel::ProjectPanel;
 use search::{BufferSearchBar, ProjectSearchBar};
 use serde::Deserialize;
 use serde_json::to_string_pretty;
-use settings::{Settings, DEFAULT_SETTINGS_ASSET_PATH};
+use settings::{KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH};
 use std::{borrow::Cow, str, sync::Arc};
 use terminal_view::terminal_button::TerminalButton;
 use util::{channel::ReleaseChannel, paths, ResultExt};
 use uuid::Uuid;
+use welcome::BaseKeymap;
 pub use workspace;
 use workspace::{
     create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow,
-    Workspace,
+    Workspace, WorkspaceSettings,
 };
 
 #[derive(Deserialize, Clone, PartialEq)]
@@ -72,8 +73,6 @@ actions!(
     ]
 );
 
-const MIN_FONT_SIZE: f32 = 6.0;
-
 pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
     cx.add_action(about);
     cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| {
@@ -117,30 +116,12 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
     cx.add_global_action(quit);
     cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
     cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
-        cx.update_global::<Settings, _, _>(|settings, cx| {
-            settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
-            if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
-                *terminal_font_size = (*terminal_font_size + 1.0).max(MIN_FONT_SIZE);
-            }
-            cx.refresh_windows();
-        });
+        theme::adjust_font_size(cx, |size| *size += 1.0)
     });
     cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
-        cx.update_global::<Settings, _, _>(|settings, cx| {
-            settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
-            if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
-                *terminal_font_size = (*terminal_font_size - 1.0).max(MIN_FONT_SIZE);
-            }
-            cx.refresh_windows();
-        });
-    });
-    cx.add_global_action(move |_: &ResetBufferFontSize, cx| {
-        cx.update_global::<Settings, _, _>(|settings, cx| {
-            settings.buffer_font_size = settings.default_buffer_font_size;
-            settings.terminal_overrides.font_size = settings.terminal_defaults.font_size;
-            cx.refresh_windows();
-        });
+        theme::adjust_font_size(cx, |size| *size -= 1.0)
     });
+    cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx));
     cx.add_global_action(move |_: &install_cli::Install, cx| {
         cx.spawn(|cx| async move {
             install_cli::install_cli(&cx)
@@ -267,10 +248,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
             }
         }
     });
-    activity_indicator::init(cx);
-    lsp_log::init(cx);
-    call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
-    settings::KeymapFileContent::load_defaults(cx);
+    load_default_keymap(cx);
 }
 
 pub fn initialize_workspace(
@@ -323,7 +301,7 @@ pub fn initialize_workspace(
     });
 
     let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
-    let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
+    let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
     let diagnostic_summary =
         cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
     let activity_indicator =
@@ -379,7 +357,7 @@ pub fn build_window_options(
 }
 
 fn quit(_: &Quit, cx: &mut gpui::AppContext) {
-    let should_confirm = cx.global::<Settings>().confirm_quit;
+    let should_confirm = settings::get::<WorkspaceSettings>(cx).confirm_quit;
     cx.spawn(|mut cx| async move {
         let mut workspaces = cx
             .window_ids()
@@ -490,6 +468,51 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
         .detach();
 }
 
+pub fn load_default_keymap(cx: &mut AppContext) {
+    for path in ["keymaps/default.json", "keymaps/vim.json"] {
+        KeymapFileContent::load_asset(path, cx).unwrap();
+    }
+
+    if let Some(asset_path) = settings::get::<BaseKeymap>(cx).asset_path() {
+        KeymapFileContent::load_asset(asset_path, cx).unwrap();
+    }
+}
+
+pub fn handle_keymap_file_changes(
+    mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
+    cx: &mut AppContext,
+) {
+    cx.spawn(move |mut cx| async move {
+        let mut settings_subscription = None;
+        while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
+            if let Ok(keymap_content) = KeymapFileContent::parse(&user_keymap_content) {
+                cx.update(|cx| {
+                    cx.clear_bindings();
+                    load_default_keymap(cx);
+                    keymap_content.clone().add_to_cx(cx).log_err();
+                });
+
+                let mut old_base_keymap = cx.read(|cx| *settings::get::<BaseKeymap>(cx));
+                drop(settings_subscription);
+                settings_subscription = Some(cx.update(|cx| {
+                    cx.observe_global::<SettingsStore, _>(move |cx| {
+                        let new_base_keymap = *settings::get::<BaseKeymap>(cx);
+                        if new_base_keymap != old_base_keymap {
+                            old_base_keymap = new_base_keymap.clone();
+
+                            cx.clear_bindings();
+                            load_default_keymap(cx);
+                            keymap_content.clone().add_to_cx(cx).log_err();
+                        }
+                    })
+                    .detach();
+                }));
+            }
+        }
+    })
+    .detach();
+}
+
 fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
     workspace.with_local_workspace(cx, move |workspace, cx| {
         let app_state = workspace.app_state().clone();
@@ -591,16 +614,21 @@ mod tests {
     use super::*;
     use assets::Assets;
     use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
-    use gpui::{executor::Deterministic, AppContext, AssetSource, TestAppContext, ViewHandle};
+    use fs::{FakeFs, Fs};
+    use gpui::{
+        elements::Empty, executor::Deterministic, Action, AnyElement, AppContext, AssetSource,
+        Element, Entity, TestAppContext, View, ViewHandle,
+    };
     use language::LanguageRegistry;
     use node_runtime::NodeRuntime;
     use project::{Project, ProjectPath};
     use serde_json::json;
+    use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
     use std::{
         collections::HashSet,
         path::{Path, PathBuf},
     };
-    use theme::ThemeRegistry;
+    use theme::{ThemeRegistry, ThemeSettings};
     use util::http::FakeHttpClient;
     use workspace::{
         item::{Item, ItemHandle},
@@ -609,7 +637,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_open_paths_action(cx: &mut TestAppContext) {
-        let app_state = init(cx);
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -709,7 +737,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
-        let app_state = init(cx);
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -789,7 +817,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_new_empty_workspace(cx: &mut TestAppContext) {
-        let app_state = init(cx);
+        let app_state = init_test(cx);
         cx.update(|cx| {
             open_new(&app_state, cx, |workspace, cx| {
                 Editor::new_file(workspace, &Default::default(), cx)
@@ -828,7 +856,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_open_entry(cx: &mut TestAppContext) {
-        let app_state = init(cx);
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -941,7 +969,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_open_paths(cx: &mut TestAppContext) {
-        let app_state = init(cx);
+        let app_state = init_test(cx);
 
         app_state
             .fs
@@ -1111,7 +1139,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_save_conflicting_item(cx: &mut TestAppContext) {
-        let app_state = init(cx);
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -1155,7 +1183,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
-        let app_state = init(cx);
+        let app_state = init_test(cx);
         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
@@ -1244,7 +1272,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
-        let app_state = init(cx);
+        let app_state = init_test(cx);
         app_state.fs.create_dir(Path::new("/root")).await.unwrap();
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
@@ -1283,9 +1311,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_pane_actions(cx: &mut TestAppContext) {
-        init(cx);
-
-        let app_state = cx.update(AppState::test);
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -1359,7 +1385,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_navigation(cx: &mut TestAppContext) {
-        let app_state = init(cx);
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -1636,7 +1662,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_reopening_closed_items(cx: &mut TestAppContext) {
-        let app_state = init(cx);
+        let app_state = init_test(cx);
         app_state
             .fs
             .as_fake()
@@ -1811,6 +1837,175 @@ mod tests {
         }
     }
 
+    #[gpui::test]
+    async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
+        struct TestView;
+
+        impl Entity for TestView {
+            type Event = ();
+        }
+
+        impl View for TestView {
+            fn ui_name() -> &'static str {
+                "TestView"
+            }
+
+            fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+                Empty::new().into_any()
+            }
+        }
+
+        let executor = cx.background();
+        let fs = FakeFs::new(executor.clone());
+
+        actions!(test, [A, B]);
+        // From the Atom keymap
+        actions!(workspace, [ActivatePreviousPane]);
+        // From the JetBrains keymap
+        actions!(pane, [ActivatePrevItem]);
+
+        fs.save(
+            "/settings.json".as_ref(),
+            &r#"
+            {
+                "base_keymap": "Atom"
+            }
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        fs.save(
+            "/keymap.json".as_ref(),
+            &r#"
+            [
+                {
+                    "bindings": {
+                        "backspace": "test::A"
+                    }
+                }
+            ]
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.update(|cx| {
+            cx.set_global(SettingsStore::test(cx));
+            theme::init(Assets, cx);
+            welcome::init(cx);
+
+            cx.add_global_action(|_: &A, _cx| {});
+            cx.add_global_action(|_: &B, _cx| {});
+            cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+            cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+            let settings_rx = watch_config_file(
+                executor.clone(),
+                fs.clone(),
+                PathBuf::from("/settings.json"),
+            );
+            let keymap_rx =
+                watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+            handle_keymap_file_changes(keymap_rx, cx);
+            handle_settings_file_changes(settings_rx, cx);
+        });
+
+        cx.foreground().run_until_parked();
+
+        let (window_id, _view) = cx.add_window(|_| TestView);
+
+        // Test loading the keymap base at all
+        assert_key_bindings_for(
+            window_id,
+            cx,
+            vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+            line!(),
+        );
+
+        // Test modifying the users keymap, while retaining the base keymap
+        fs.save(
+            "/keymap.json".as_ref(),
+            &r#"
+            [
+                {
+                    "bindings": {
+                        "backspace": "test::B"
+                    }
+                }
+            ]
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.foreground().run_until_parked();
+
+        assert_key_bindings_for(
+            window_id,
+            cx,
+            vec![("backspace", &B), ("k", &ActivatePreviousPane)],
+            line!(),
+        );
+
+        // Test modifying the base, while retaining the users keymap
+        fs.save(
+            "/settings.json".as_ref(),
+            &r#"
+            {
+                "base_keymap": "JetBrains"
+            }
+            "#
+            .into(),
+            Default::default(),
+        )
+        .await
+        .unwrap();
+
+        cx.foreground().run_until_parked();
+
+        assert_key_bindings_for(
+            window_id,
+            cx,
+            vec![("backspace", &B), ("[", &ActivatePrevItem)],
+            line!(),
+        );
+
+        fn assert_key_bindings_for<'a>(
+            window_id: usize,
+            cx: &TestAppContext,
+            actions: Vec<(&'static str, &'a dyn Action)>,
+            line: u32,
+        ) {
+            for (key, action) in actions {
+                // assert that...
+                assert!(
+                    cx.available_actions(window_id, 0)
+                        .into_iter()
+                        .any(|(_, bound_action, b)| {
+                            // action names match...
+                            bound_action.name() == action.name()
+                        && bound_action.namespace() == action.namespace()
+                        // and key strokes contain the given key
+                        && b.iter()
+                            .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+                        }),
+                    "On {} Failed to find {} with key binding {}",
+                    line,
+                    action.name(),
+                    key
+                );
+            }
+        }
+    }
+
     #[gpui::test]
     fn test_bundled_settings_and_themes(cx: &mut AppContext) {
         cx.platform()
@@ -1829,15 +2024,20 @@ mod tests {
             ])
             .unwrap();
         let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
-        let settings = Settings::defaults(Assets, cx.font_cache(), &themes);
+        let mut settings = SettingsStore::default();
+        settings
+            .set_default_settings(&settings::default_settings(), cx)
+            .unwrap();
+        cx.set_global(settings);
+        theme::init(Assets, cx);
 
         let mut has_default_theme = false;
         for theme_name in themes.list(false).map(|meta| meta.name) {
             let theme = themes.get(&theme_name).unwrap();
-            if theme.meta.name == settings.theme.meta.name {
+            assert_eq!(theme.meta.name, theme_name);
+            if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
                 has_default_theme = true;
             }
-            assert_eq!(theme.meta.name, theme_name);
         }
         assert!(has_default_theme);
     }
@@ -1847,25 +2047,26 @@ mod tests {
         let mut languages = LanguageRegistry::test();
         languages.set_executor(cx.background().clone());
         let languages = Arc::new(languages);
-        let themes = ThemeRegistry::new((), cx.font_cache().clone());
         let http = FakeHttpClient::with_404_response();
         let node_runtime = NodeRuntime::new(http, cx.background().to_owned());
-        languages::init(languages.clone(), themes, node_runtime);
+        languages::init(languages.clone(), node_runtime);
         for name in languages.language_names() {
             languages.language_for_name(&name);
         }
         cx.foreground().run_until_parked();
     }
 
-    fn init(cx: &mut TestAppContext) -> Arc<AppState> {
+    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
         cx.foreground().forbid_parking();
         cx.update(|cx| {
             let mut app_state = AppState::test(cx);
             let state = Arc::get_mut(&mut app_state).unwrap();
             state.initialize_workspace = initialize_workspace;
             state.build_window_options = build_window_options;
+            theme::init((), cx);
             call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
             workspace::init(app_state.clone(), cx);
+            language::init(cx);
             editor::init(cx);
             pane::init(cx);
             app_state