Create proof-of-concept SettingStore struct

Max Brunsfeld created

Change summary

Cargo.lock                            | 352 +++++++++-------
crates/settings/Cargo.toml            |   1 
crates/settings/src/settings.rs       |   1 
crates/settings/src/settings_store.rs | 608 +++++++++++++++++++++++++++++
crates/util/src/util.rs               |  21 +
5 files changed, 826 insertions(+), 157 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -14,7 +14,7 @@ version = "0.1.0"
 dependencies = [
  "auto_update",
  "editor",
- "futures 0.3.25",
+ "futures 0.3.28",
  "gpui",
  "language",
  "project",
@@ -30,7 +30,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 +60,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 +85,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 +111,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=a51dbe25d67e84d6ed
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -92,7 +121,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 +174,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 +261,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 +302,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 +346,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 +363,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 +376,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 +400,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 +418,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 +448,7 @@ dependencies = [
  "filetime",
  "libc",
  "pin-project",
- "redox_syscall",
+ "redox_syscall 0.2.16",
  "xattr",
 ]
 
@@ -443,13 +472,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 +515,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 +577,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 +611,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 +627,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 +643,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 +666,7 @@ dependencies = [
  "proc-macro-error",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -646,11 +675,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 +706,7 @@ dependencies = [
  "cexpr",
  "clang-sys",
  "clap 2.34.0",
- "env_logger",
+ "env_logger 0.9.3",
  "lazy_static",
  "lazycell",
  "log",
@@ -707,18 +742,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 +761,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 +839,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 +899,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 +912,7 @@ dependencies = [
  "client",
  "collections",
  "fs",
- "futures 0.3.25",
+ "futures 0.3.28",
  "gpui",
  "language",
  "live_kit_client",
@@ -894,7 +932,7 @@ checksum = "e54b86398b5852ddd45784b1d9b196b98beb39171821bad4b8b44534a1e87927"
 dependencies = [
  "cap-primitives",
  "cap-std",
- "io-lifetimes",
+ "io-lifetimes 0.5.3",
  "winapi 0.3.9",
 ]
 
@@ -905,13 +943,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 +973,9 @@ checksum = "2247568946095c7765ad2b441a56caffc08027734c634a6d5edda648f04e32eb"
 dependencies = [
  "cap-primitives",
  "io-extras",
- "io-lifetimes",
+ "io-lifetimes 0.5.3",
  "ipnet",
- "rustix",
+ "rustix 0.33.7",
 ]
 
 [[package]]
@@ -948,7 +986,7 @@ checksum = "c50472b6ebc302af0401fa3fb939694cd8ff00e0d4c9182001e434fc822ab83a"
 dependencies = [
  "cap-primitives",
  "once_cell",
- "rustix",
+ "rustix 0.33.7",
  "winx",
 ]
 
@@ -960,9 +998,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 +1028,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 +1044,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 +1059,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 +1085,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 +1102,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 +1127,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",
@@ -1108,7 +1146,7 @@ dependencies = [
  "async-tungstenite",
  "collections",
  "db",
- "futures 0.3.25",
+ "futures 0.3.28",
  "gpui",
  "image",
  "lazy_static",
@@ -1125,11 +1163,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]]
@@ -1141,9 +1179,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",
 ]
@@ -1195,18 +1233,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",
@@ -1236,7 +1274,7 @@ dependencies = [
  "sha-1 0.9.8",
  "sqlx",
  "theme",
- "time 0.3.17",
+ "time 0.3.21",
  "tokio",
  "tokio-tungstenite",
  "toml",
@@ -1263,7 +1301,7 @@ dependencies = [
  "context_menu",
  "editor",
  "feedback",
- "futures 0.3.25",
+ "futures 0.3.28",
  "fuzzy",
  "gpui",
  "log",
@@ -1299,7 +1337,7 @@ dependencies = [
  "collections",
  "ctor",
  "editor",
- "env_logger",
+ "env_logger 0.9.3",
  "fuzzy",
  "gpui",
  "picker",
@@ -1313,11 +1351,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]]
@@ -1342,7 +1380,7 @@ dependencies = [
  "collections",
  "context_menu",
  "fs",
- "futures 0.3.25",
+ "futures 0.3.28",
  "gpui",
  "language",
  "log",
@@ -1366,7 +1404,7 @@ dependencies = [
  "context_menu",
  "copilot",
  "editor",
- "futures 0.3.25",
+ "futures 0.3.28",
  "gpui",
  "settings",
  "smol",
@@ -1445,9 +1483,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",
 ]
@@ -1472,7 +1510,7 @@ dependencies = [
  "cranelift-codegen-shared",
  "cranelift-entity",
  "cranelift-isle",
- "gimli",
+ "gimli 0.26.2",
  "log",
  "regalloc2",
  "smallvec",
@@ -1550,18 +1588,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"
@@ -1584,35 +1622,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",
 ]
 
@@ -1623,7 +1661,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]]
@@ -1639,9 +1677,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",
 ]
@@ -1673,7 +1711,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
 dependencies = [
  "quote",
- "syn",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1693,9 +1731,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",
@@ -1709,9 +1747,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",
@@ -1721,9 +1759,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",
@@ -1731,24 +1769,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]]
@@ -1761,7 +1799,7 @@ dependencies = [
  "hashbrown 0.12.3",
  "lock_api",
  "once_cell",
- "parking_lot_core 0.9.5",
+ "parking_lot_core 0.9.7",
 ]
 
 [[package]]
@@ -1780,7 +1818,7 @@ dependencies = [
  "anyhow",
  "async-trait",
  "collections",
- "env_logger",
+ "env_logger 0.9.3",
  "gpui",
  "indoc",
  "lazy_static",
@@ -1864,7 +1902,7 @@ version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
 dependencies = [
- "block-buffer 0.10.3",
+ "block-buffer 0.10.4",
  "crypto-common",
  "subtle",
 ]

crates/settings/Cargo.toml 🔗

@@ -31,6 +31,7 @@ schemars = "0.8"
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
+smallvec.workspace = true
 toml = "0.5"
 tree-sitter = "*"
 tree-sitter-json = "*"

crates/settings/src/settings_store.rs 🔗

@@ -0,0 +1,608 @@
+use anyhow::{anyhow, Result};
+use collections::{hash_map, BTreeMap, HashMap, HashSet};
+use schemars::JsonSchema;
+use serde::{de::DeserializeOwned, Serialize};
+use serde_json::value::RawValue;
+use smallvec::SmallVec;
+use std::{
+    any::{type_name, Any, TypeId},
+    cmp::Ordering,
+    fmt::Debug,
+    mem,
+    path::Path,
+    sync::Arc,
+};
+use util::{merge_non_null_json_value_into, 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 + Debug {
+    /// The name of a field 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 FIELD_NAME: Option<&'static str> = None;
+
+    /// The type that is stored in an individual JSON file.
+    type FileContent: 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]) -> Self;
+
+    fn load_via_json_merge(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+    ) -> Self
+    where
+        Self: DeserializeOwned,
+        Self::FileContent: Serialize,
+    {
+        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);
+        }
+        serde_json::from_value(merged).unwrap()
+    }
+}
+
+/// A set of strongly-typed setting values defined via multiple JSON files.
+#[derive(Default)]
+pub struct SettingsStore {
+    setting_keys: Vec<(Option<&'static str>, TypeId)>,
+    setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
+    default_deserialized_settings: DeserializedSettingMap,
+    user_deserialized_settings: Option<DeserializedSettingMap>,
+    local_deserialized_settings: BTreeMap<Arc<Path>, DeserializedSettingMap>,
+    changed_setting_types: HashSet<TypeId>,
+}
+
+#[derive(Debug)]
+struct SettingValue<T> {
+    global_value: Option<T>,
+    local_values: Vec<(Arc<Path>, T)>,
+}
+
+trait AnySettingValue: Debug {
+    fn setting_type_name(&self) -> &'static str;
+    fn deserialize_setting(&self, json: &str) -> Result<DeserializedSetting>;
+    fn load_setting(
+        &self,
+        default_value: &DeserializedSetting,
+        custom: &[&DeserializedSetting],
+    ) -> 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>);
+}
+
+struct DeserializedSetting(Box<dyn Any>);
+
+type DeserializedSettingMap = HashMap<TypeId, DeserializedSetting>;
+
+impl SettingsStore {
+    /// Add a new type of setting to the store.
+    ///
+    /// This should be done before any settings are loaded.
+    pub fn register_setting<T: Setting>(&mut self) {
+        let type_id = TypeId::of::<T>();
+
+        let entry = self.setting_values.entry(type_id);
+        if matches!(entry, hash_map::Entry::Occupied(_)) {
+            panic!("duplicate setting type: {}", type_name::<T>());
+        }
+        entry.or_insert(Box::new(SettingValue::<T> {
+            global_value: None,
+            local_values: Vec::new(),
+        }));
+
+        match self
+            .setting_keys
+            .binary_search_by_key(&T::FIELD_NAME, |e| e.0)
+        {
+            Ok(ix) | Err(ix) => self.setting_keys.insert(ix, (T::FIELD_NAME, type_id)),
+        }
+    }
+
+    /// Get the value of a setting.
+    ///
+    /// Panics if settings have not yet been loaded, or there is no default
+    /// value for this setting.
+    pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T {
+        self.setting_values
+            .get(&TypeId::of::<T>())
+            .unwrap()
+            .value_for_path(path)
+            .downcast_ref::<T>()
+            .unwrap()
+    }
+
+    /// 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) -> Result<()> {
+        self.default_deserialized_settings = self.load_setting_map(default_settings_content)?;
+        if self.default_deserialized_settings.len() != self.setting_keys.len() {
+            return Err(anyhow!(
+                "default settings file is missing fields: {:?}",
+                self.setting_keys
+                    .iter()
+                    .filter(|(_, type_id)| !self
+                        .default_deserialized_settings
+                        .contains_key(type_id))
+                    .map(|(name, _)| *name)
+                    .collect::<Vec<_>>()
+            ));
+        }
+        self.recompute_values(false, None, None);
+        Ok(())
+    }
+
+    /// Set the user settings via a JSON string.
+    pub fn set_user_settings(&mut self, user_settings_content: &str) -> Result<()> {
+        let user_settings = self.load_setting_map(user_settings_content)?;
+        let old_user_settings =
+            mem::replace(&mut self.user_deserialized_settings, Some(user_settings));
+        self.recompute_values(true, None, old_user_settings);
+        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>,
+    ) -> Result<()> {
+        let removed_map = if let Some(settings_content) = settings_content {
+            self.local_deserialized_settings
+                .insert(path.clone(), self.load_setting_map(settings_content)?);
+            None
+        } else {
+            self.local_deserialized_settings.remove(&path)
+        };
+        self.recompute_values(true, Some(&path), removed_map);
+        Ok(())
+    }
+
+    fn recompute_values(
+        &mut self,
+        user_settings_changed: bool,
+        changed_local_path: Option<&Path>,
+        old_settings_map: Option<DeserializedSettingMap>,
+    ) {
+        // Identify all of the setting types that have changed.
+        let new_settings_map = if let Some(changed_path) = changed_local_path {
+            &self.local_deserialized_settings.get(changed_path).unwrap()
+        } else if user_settings_changed {
+            self.user_deserialized_settings.as_ref().unwrap()
+        } else {
+            &self.default_deserialized_settings
+        };
+        self.changed_setting_types.clear();
+        self.changed_setting_types.extend(new_settings_map.keys());
+        if let Some(previous_settings_map) = old_settings_map {
+            self.changed_setting_types
+                .extend(previous_settings_map.keys());
+        }
+
+        // Reload the global and local values for every changed setting.
+        let mut user_values_stack = Vec::<&DeserializedSetting>::new();
+        for setting_type_id in self.changed_setting_types.iter() {
+            let setting_value = self.setting_values.get_mut(setting_type_id).unwrap();
+
+            // Build the prioritized list of deserialized values to pass to the setting's
+            // load function.
+            user_values_stack.clear();
+            if let Some(user_settings) = &self.user_deserialized_settings {
+                if let Some(user_value) = user_settings.get(setting_type_id) {
+                    user_values_stack.push(&user_value);
+                }
+            }
+
+            // If the global settings file changed, reload the global value for the field.
+            if changed_local_path.is_none() {
+                let global_value = setting_value.load_setting(
+                    &self.default_deserialized_settings[setting_type_id],
+                    &user_values_stack,
+                );
+                setting_value.set_global_value(global_value);
+            }
+
+            // Reload the local values for the setting.
+            let user_value_stack_len = user_values_stack.len();
+            for (path, deserialized_values) in &self.local_deserialized_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;
+                }
+
+                // Ignore recomputing settings for any path that hasn't customized that setting.
+                let Some(deserialized_value) = deserialized_values.get(setting_type_id) else {
+                    continue;
+                };
+
+                // Build a stack of all of the local values for that setting.
+                user_values_stack.truncate(user_value_stack_len);
+                for (preceding_path, preceding_deserialized_values) in
+                    &self.local_deserialized_settings
+                {
+                    if preceding_path >= path {
+                        break;
+                    }
+                    if !path.starts_with(preceding_path) {
+                        continue;
+                    }
+
+                    if let Some(preceding_deserialized_value) =
+                        preceding_deserialized_values.get(setting_type_id)
+                    {
+                        user_values_stack.push(&*preceding_deserialized_value);
+                    }
+                }
+                user_values_stack.push(&*deserialized_value);
+
+                // Load the local value for the field.
+                let local_value = setting_value.load_setting(
+                    &self.default_deserialized_settings[setting_type_id],
+                    &user_values_stack,
+                );
+                setting_value.set_local_value(path.clone(), local_value);
+            }
+        }
+    }
+
+    /// Deserialize the given JSON string into a map keyed by setting type.
+    ///
+    /// Returns an error if the string doesn't contain a valid JSON object.
+    fn load_setting_map(&self, json: &str) -> Result<DeserializedSettingMap> {
+        let mut map = DeserializedSettingMap::default();
+        let settings_content_by_key: BTreeMap<&str, &RawValue> = serde_json::from_str(json)?;
+        let mut setting_types_by_key = self.setting_keys.iter().peekable();
+
+        // Load all of the fields that don't have a key.
+        while let Some((setting_key, setting_type_id)) = setting_types_by_key.peek() {
+            if setting_key.is_some() {
+                break;
+            }
+            setting_types_by_key.next();
+            if let Some(deserialized_value) = self
+                .setting_values
+                .get(setting_type_id)
+                .unwrap()
+                .deserialize_setting(json)
+                .log_err()
+            {
+                map.insert(*setting_type_id, deserialized_value);
+            }
+        }
+
+        // For each key in the file, load all of the settings that belong to that key.
+        for (key, key_content) in settings_content_by_key {
+            while let Some((setting_key, setting_type_id)) = setting_types_by_key.peek() {
+                let setting_key = setting_key.expect("setting names are ordered");
+                match setting_key.cmp(key) {
+                    Ordering::Less => {
+                        setting_types_by_key.next();
+                        continue;
+                    }
+                    Ordering::Greater => break,
+                    Ordering::Equal => {
+                        if let Some(deserialized_value) = self
+                            .setting_values
+                            .get(setting_type_id)
+                            .unwrap()
+                            .deserialize_setting(key_content.get())
+                            .log_err()
+                        {
+                            map.insert(*setting_type_id, deserialized_value);
+                        }
+                        setting_types_by_key.next();
+                    }
+                }
+            }
+        }
+        Ok(map)
+    }
+}
+
+impl<T: Setting> AnySettingValue for SettingValue<T> {
+    fn setting_type_name(&self) -> &'static str {
+        type_name::<T>()
+    }
+
+    fn load_setting(
+        &self,
+        default_value: &DeserializedSetting,
+        user_values: &[&DeserializedSetting],
+    ) -> 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();
+        Box::new(T::load(default_value, &values))
+    }
+
+    fn deserialize_setting(&self, json: &str) -> Result<DeserializedSetting> {
+        let value = serde_json::from_str::<T::FileContent>(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()
+    }
+
+    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)),
+        }
+    }
+}
+
+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();
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use serde_derive::Deserialize;
+
+    #[test]
+    fn test_settings_store() {
+        let mut store = SettingsStore::default();
+        store.register_setting::<UserSettings>();
+        store.register_setting::<TurboSetting>();
+        store.register_setting::<MultiKeySettings>();
+
+        // error - missing required field in default settings
+        store
+            .set_default_settings(
+                r#"{
+                    "user": {
+                        "name": "John Doe",
+                        "age": 30,
+                        "staff": false
+                    }
+                }"#,
+            )
+            .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
+                    }
+                }"#,
+            )
+            .unwrap_err();
+
+        // valid default settings.
+        store
+            .set_default_settings(
+                r#"{
+                    "turbo": false,
+                    "user": {
+                        "name": "John Doe",
+                        "age": 30,
+                        "staff": false
+                    }
+                }"#,
+            )
+            .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"
+                }"#,
+            )
+            .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 } }"#),
+            )
+            .unwrap();
+        store
+            .set_local_settings(
+                Path::new("/root1/subdir").into(),
+                Some(r#"{ "user": { "name": "Jane Doe" } }"#),
+            )
+            .unwrap();
+
+        store
+            .set_local_settings(
+                Path::new("/root2").into(),
+                Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
+            )
+            .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(),
+            }
+        );
+    }
+
+    #[derive(Debug, PartialEq, Deserialize)]
+    struct UserSettings {
+        name: String,
+        age: u32,
+        staff: bool,
+    }
+
+    #[derive(Serialize, Deserialize, JsonSchema)]
+    struct UserSettingsJson {
+        name: Option<String>,
+        age: Option<u32>,
+        staff: Option<bool>,
+    }
+
+    impl Setting for UserSettings {
+        const FIELD_NAME: Option<&'static str> = Some("user");
+        type FileContent = UserSettingsJson;
+
+        fn load(default_value: &UserSettingsJson, user_values: &[&UserSettingsJson]) -> Self {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct TurboSetting(bool);
+
+    impl Setting for TurboSetting {
+        const FIELD_NAME: Option<&'static str> = Some("turbo");
+        type FileContent = Option<bool>;
+
+        fn load(default_value: &Option<bool>, user_values: &[&Option<bool>]) -> 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(Serialize, Deserialize, JsonSchema)]
+    struct MultiKeySettingsJson {
+        key1: Option<String>,
+        key2: Option<String>,
+    }
+
+    impl Setting for MultiKeySettings {
+        type FileContent = MultiKeySettingsJson;
+
+        fn load(
+            default_value: &MultiKeySettingsJson,
+            user_values: &[&MultiKeySettingsJson],
+        ) -> 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 FIELD_NAME: Option<&'static str> = Some("journal");
+
+        type FileContent = JournalSettingsJson;
+
+        fn load(default_value: &JournalSettingsJson, user_values: &[&JournalSettingsJson]) -> Self {
+            Self::load_via_json_merge(default_value, user_values)
+        }
+    }
+}

crates/util/src/util.rs 🔗

@@ -93,6 +93,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;