Fallible Settings (#42938)

Conrad Irwin created

Also tidies up error notifications so that in the case of syntax errors
we don't see noise about the migration failing as well.

Release Notes:

- Invalid values in settings files will no longer prevent the rest of
the file from being parsed.

Change summary

Cargo.lock                                             | 288 ++++-------
Cargo.toml                                             |   1 
crates/settings/Cargo.toml                             |   1 
crates/settings/src/fallible_options.rs                | 112 ++++
crates/settings/src/settings.rs                        |   3 
crates/settings/src/settings_content.rs                |  59 +-
crates/settings/src/settings_content/agent.rs          |  19 
crates/settings/src/settings_content/editor.rs         |  23 
crates/settings/src/settings_content/extension.rs      |   6 
crates/settings/src/settings_content/language.rs       |  38 
crates/settings/src/settings_content/language_model.rs |  63 +-
crates/settings/src/settings_content/project.rs        |  45 -
crates/settings/src/settings_content/terminal.rs       |  11 
crates/settings/src/settings_content/theme.rs          |  56 -
crates/settings/src/settings_content/workspace.rs      |  25 
crates/settings/src/settings_store.rs                  |  37 -
crates/settings_macros/src/settings_macros.rs          |  52 ++
crates/zed/src/main.rs                                 |  11 
crates/zed/src/zed.rs                                  | 151 ++---
19 files changed, 508 insertions(+), 493 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -183,7 +183,7 @@ dependencies = [
  "regex",
  "reqwest_client",
  "rust-embed",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -238,7 +238,7 @@ checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
 dependencies = [
  "anyhow",
  "derive_more 2.0.1",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
 ]
@@ -298,7 +298,7 @@ dependencies = [
  "language_model",
  "paths",
  "project",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -372,7 +372,7 @@ dependencies = [
  "release_channel",
  "rope",
  "rules_library",
- "schemars 1.0.4",
+ "schemars",
  "search",
  "serde",
  "serde_json",
@@ -627,7 +627,7 @@ dependencies = [
  "chrono",
  "futures 0.3.31",
  "http_client",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -2033,7 +2033,7 @@ dependencies = [
  "aws-sdk-bedrockruntime",
  "aws-smithy-types",
  "futures 0.3.31",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "strum 0.27.2",
@@ -2578,7 +2578,7 @@ version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201"
 dependencies = [
- "darling 0.20.11",
+ "darling",
  "proc-macro2",
  "quote",
  "syn 2.0.106",
@@ -2837,7 +2837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff"
 dependencies = [
  "heck 0.4.1",
- "indexmap 2.11.4",
+ "indexmap",
  "log",
  "proc-macro2",
  "quote",
@@ -3207,7 +3207,7 @@ dependencies = [
  "indoc",
  "ordered-float 2.10.1",
  "rustc-hash 2.1.1",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "strum 0.27.2",
@@ -3484,7 +3484,7 @@ dependencies = [
 name = "collections"
 version = "0.1.0"
 dependencies = [
- "indexmap 2.11.4",
+ "indexmap",
  "rustc-hash 2.1.1",
 ]
 
@@ -3714,7 +3714,7 @@ dependencies = [
  "net",
  "parking_lot",
  "postage",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -4464,7 +4464,7 @@ checksum = "d74b6bcf49ebbd91f1b1875b706ea46545032a14003b5557b7dfa4bbeba6766e"
 dependencies = [
  "cc",
  "codespan-reporting 0.13.0",
- "indexmap 2.11.4",
+ "indexmap",
  "proc-macro2",
  "quote",
  "scratch",
@@ -4479,7 +4479,7 @@ checksum = "94ca2ad69673c4b35585edfa379617ac364bccd0ba0adf319811ba3a74ffa48a"
 dependencies = [
  "clap",
  "codespan-reporting 0.13.0",
- "indexmap 2.11.4",
+ "indexmap",
  "proc-macro2",
  "quote",
  "syn 2.0.106",
@@ -4497,7 +4497,7 @@ version = "1.0.187"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2a8ebf0b6138325af3ec73324cb3a48b64d57721f17291b151206782e61f66cd"
 dependencies = [
- "indexmap 2.11.4",
+ "indexmap",
  "proc-macro2",
  "quote",
  "syn 2.0.106",
@@ -4526,7 +4526,7 @@ dependencies = [
  "parking_lot",
  "paths",
  "proto",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -4545,7 +4545,7 @@ name = "dap-types"
 version = "0.0.1"
 source = "git+https://github.com/zed-industries/dap-types?rev=1b461b310481d01e02b2603c16d7144b926339f8#1b461b310481d01e02b2603c16d7144b926339f8"
 dependencies = [
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
 ]
@@ -4582,18 +4582,8 @@ version = "0.20.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
 dependencies = [
- "darling_core 0.20.11",
- "darling_macro 0.20.11",
-]
-
-[[package]]
-name = "darling"
-version = "0.21.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
-dependencies = [
- "darling_core 0.21.3",
- "darling_macro 0.21.3",
+ "darling_core",
+ "darling_macro",
 ]
 
 [[package]]
@@ -4610,38 +4600,13 @@ dependencies = [
  "syn 2.0.106",
 ]
 
-[[package]]
-name = "darling_core"
-version = "0.21.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
-dependencies = [
- "fnv",
- "ident_case",
- "proc-macro2",
- "quote",
- "strsim",
- "syn 2.0.106",
-]
-
 [[package]]
 name = "darling_macro"
 version = "0.20.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
 dependencies = [
- "darling_core 0.20.11",
- "quote",
- "syn 2.0.106",
-]
-
-[[package]]
-name = "darling_macro"
-version = "0.21.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
-dependencies = [
- "darling_core 0.21.3",
+ "darling_core",
  "quote",
  "syn 2.0.106",
 ]
@@ -4791,7 +4756,7 @@ dependencies = [
  "pretty_assertions",
  "project",
  "rpc",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -4830,7 +4795,7 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
 ]
@@ -4945,7 +4910,7 @@ version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9"
 dependencies = [
- "darling 0.20.11",
+ "darling",
  "proc-macro2",
  "quote",
  "syn 2.0.106",
@@ -5422,7 +5387,7 @@ dependencies = [
  "release_channel",
  "rope",
  "rpc",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -6154,7 +6119,7 @@ dependencies = [
  "picker",
  "pretty_assertions",
  "project",
- "schemars 1.0.4",
+ "schemars",
  "search",
  "serde",
  "serde_json",
@@ -7001,7 +6966,7 @@ dependencies = [
  "derive_more 2.0.1",
  "derive_setters",
  "gh-workflow-macros",
- "indexmap 2.11.4",
+ "indexmap",
  "merge",
  "serde",
  "serde_json",
@@ -7036,7 +7001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 dependencies = [
  "fallible-iterator",
- "indexmap 2.11.4",
+ "indexmap",
  "stable_deref_trait",
 ]
 
@@ -7066,7 +7031,7 @@ dependencies = [
  "rand 0.9.2",
  "regex",
  "rope",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "smol",
@@ -7152,7 +7117,7 @@ dependencies = [
  "project",
  "recent_projects",
  "remote",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -7256,7 +7221,7 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -7360,7 +7325,7 @@ dependencies = [
  "refineable",
  "reqwest_client",
  "resvg",
- "schemars 1.0.4",
+ "schemars",
  "seahash",
  "semantic_version",
  "serde",
@@ -7448,7 +7413,7 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "http 0.2.12",
- "indexmap 2.11.4",
+ "indexmap",
  "slab",
  "tokio",
  "tokio-util",
@@ -7467,7 +7432,7 @@ dependencies = [
  "futures-core",
  "futures-sink",
  "http 1.3.1",
- "indexmap 2.11.4",
+ "indexmap",
  "slab",
  "tokio",
  "tokio-util",
@@ -8230,17 +8195,6 @@ version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
 
-[[package]]
-name = "indexmap"
-version = "1.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
-dependencies = [
- "autocfg",
- "hashbrown 0.12.3",
- "serde",
-]
-
 [[package]]
 name = "indexmap"
 version = "2.11.4"
@@ -8629,7 +8583,7 @@ dependencies = [
  "language",
  "paths",
  "project",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -8830,7 +8784,7 @@ dependencies = [
  "rand 0.9.2",
  "regex",
  "rpc",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -8903,7 +8857,7 @@ dependencies = [
  "open_router",
  "parking_lot",
  "proto",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -8952,7 +8906,7 @@ dependencies = [
  "partial-json-fixer",
  "project",
  "release_channel",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -9469,7 +9423,7 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
 ]
@@ -9532,7 +9486,7 @@ dependencies = [
  "parking_lot",
  "postage",
  "release_channel",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "smol",
@@ -10112,7 +10066,7 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "strum 0.27.2",
@@ -10212,7 +10166,7 @@ dependencies = [
  "half",
  "hashbrown 0.15.5",
  "hexf-parse",
- "indexmap 2.11.4",
+ "indexmap",
  "log",
  "num-traits",
  "once_cell",
@@ -10886,7 +10840,7 @@ checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
 dependencies = [
  "crc32fast",
  "hashbrown 0.15.5",
- "indexmap 2.11.4",
+ "indexmap",
  "memchr",
 ]
 
@@ -10941,7 +10895,7 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -10964,7 +10918,7 @@ dependencies = [
  "notifications",
  "picker",
  "project",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "settings",
  "telemetry",
@@ -11049,7 +11003,7 @@ dependencies = [
  "futures 0.3.31",
  "http_client",
  "log",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -11064,7 +11018,7 @@ dependencies = [
  "anyhow",
  "futures 0.3.31",
  "http_client",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -11945,7 +11899,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
 dependencies = [
  "fixedbitset",
- "indexmap 2.11.4",
+ "indexmap",
 ]
 
 [[package]]
@@ -12061,7 +12015,7 @@ dependencies = [
  "env_logger 0.11.8",
  "gpui",
  "menu",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "theme",
@@ -12178,7 +12132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
 dependencies = [
  "base64 0.22.1",
- "indexmap 2.11.4",
+ "indexmap",
  "quick-xml 0.38.3",
  "serde",
  "time",
@@ -12341,7 +12295,7 @@ dependencies = [
  "comfy-table",
  "either",
  "hashbrown 0.15.5",
- "indexmap 2.11.4",
+ "indexmap",
  "itoa",
  "num-traits",
  "polars-arrow",
@@ -12518,7 +12472,7 @@ dependencies = [
  "either",
  "hashbrown 0.15.5",
  "hex",
- "indexmap 2.11.4",
+ "indexmap",
  "libm",
  "memchr",
  "num-traits",
@@ -12630,7 +12584,7 @@ version = "0.51.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e6c1ab13e04d5167661a9854ed1ea0482b2ed9b8a0f1118dabed7cd994a85e3"
 dependencies = [
- "indexmap 2.11.4",
+ "indexmap",
  "polars-error",
  "polars-utils",
  "serde",
@@ -12733,7 +12687,7 @@ dependencies = [
  "flate2",
  "foldhash 0.1.5",
  "hashbrown 0.15.5",
- "indexmap 2.11.4",
+ "indexmap",
  "libc",
  "memmap2",
  "num-traits",
@@ -13059,7 +13013,7 @@ dependencies = [
  "gpui",
  "http_client",
  "image",
- "indexmap 2.11.4",
+ "indexmap",
  "itertools 0.14.0",
  "language",
  "log",
@@ -13076,7 +13030,7 @@ dependencies = [
  "release_channel",
  "remote",
  "rpc",
- "schemars 1.0.4",
+ "schemars",
  "semver",
  "serde",
  "serde_json",
@@ -13141,7 +13095,7 @@ dependencies = [
  "pretty_assertions",
  "project",
  "rayon",
- "schemars 1.0.4",
+ "schemars",
  "search",
  "serde",
  "serde_json",
@@ -14010,7 +13964,7 @@ dependencies = [
  "prost 0.9.0",
  "release_channel",
  "rpc",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -14900,24 +14854,12 @@ dependencies = [
  "anyhow",
  "clap",
  "env_logger 0.11.8",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "theme",
 ]
 
-[[package]]
-name = "schemars"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
-dependencies = [
- "dyn-clone",
- "ref-cast",
- "serde",
- "serde_json",
-]
-
 [[package]]
 name = "schemars"
 version = "1.0.4"
@@ -14925,7 +14867,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
 dependencies = [
  "dyn-clone",
- "indexmap 2.11.4",
+ "indexmap",
  "ref-cast",
  "schemars_derive",
  "serde",
@@ -15138,7 +15080,7 @@ dependencies = [
  "menu",
  "pretty_assertions",
  "project",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -15288,7 +15230,7 @@ version = "1.0.145"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
 dependencies = [
- "indexmap 2.11.4",
+ "indexmap",
  "itoa",
  "memchr",
  "ryu",
@@ -15302,7 +15244,7 @@ version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540"
 dependencies = [
- "indexmap 2.11.4",
+ "indexmap",
  "itoa",
  "memchr",
  "ryu",
@@ -15372,44 +15314,13 @@ dependencies = [
  "serde",
 ]
 
-[[package]]
-name = "serde_with"
-version = "3.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5"
-dependencies = [
- "base64 0.22.1",
- "chrono",
- "hex",
- "indexmap 1.9.3",
- "indexmap 2.11.4",
- "schemars 0.9.0",
- "schemars 1.0.4",
- "serde_core",
- "serde_json",
- "serde_with_macros",
- "time",
-]
-
-[[package]]
-name = "serde_with_macros"
-version = "3.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27"
-dependencies = [
- "darling 0.21.3",
- "proc-macro2",
- "quote",
- "syn 2.0.106",
-]
-
 [[package]]
 name = "serde_yaml"
 version = "0.9.34+deprecated"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
 dependencies = [
- "indexmap 2.11.4",
+ "indexmap",
  "itoa",
  "ryu",
  "serde",
@@ -15457,12 +15368,11 @@ dependencies = [
  "pretty_assertions",
  "release_channel",
  "rust-embed",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "serde_json_lenient",
  "serde_repr",
- "serde_with",
  "settings_json",
  "settings_macros",
  "smallvec",
@@ -15541,7 +15451,7 @@ dependencies = [
  "pretty_assertions",
  "project",
  "release_channel",
- "schemars 1.0.4",
+ "schemars",
  "search",
  "serde",
  "session",
@@ -15845,7 +15755,7 @@ dependencies = [
  "indoc",
  "parking_lot",
  "paths",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -16051,7 +15961,7 @@ dependencies = [
  "futures-util",
  "hashbrown 0.15.5",
  "hashlink 0.10.0",
- "indexmap 2.11.4",
+ "indexmap",
  "log",
  "memchr",
  "once_cell",
@@ -16990,7 +16900,7 @@ dependencies = [
  "menu",
  "picker",
  "project",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "settings",
@@ -17069,7 +16979,7 @@ dependencies = [
  "parking_lot",
  "pretty_assertions",
  "proto",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -17172,7 +17082,7 @@ dependencies = [
  "rand 0.9.2",
  "regex",
  "release_channel",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "settings",
  "smol",
@@ -17218,7 +17128,7 @@ dependencies = [
  "project",
  "rand 0.9.2",
  "regex",
- "schemars 1.0.4",
+ "schemars",
  "search",
  "serde",
  "serde_json",
@@ -17270,7 +17180,7 @@ dependencies = [
  "palette",
  "parking_lot",
  "refineable",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -17300,7 +17210,7 @@ dependencies = [
  "clap",
  "collections",
  "gpui",
- "indexmap 2.11.4",
+ "indexmap",
  "log",
  "palette",
  "serde",
@@ -17554,7 +17464,7 @@ dependencies = [
  "project",
  "remote",
  "rpc",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "settings",
  "smallvec",
@@ -17743,7 +17653,7 @@ version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
 dependencies = [
- "indexmap 2.11.4",
+ "indexmap",
  "serde_core",
  "serde_spanned 1.0.3",
  "toml_datetime 0.7.3",
@@ -17776,7 +17686,7 @@ version = "0.22.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
 dependencies = [
- "indexmap 2.11.4",
+ "indexmap",
  "serde",
  "serde_spanned 0.6.9",
  "toml_datetime 0.6.11",
@@ -17790,7 +17700,7 @@ version = "0.23.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
 dependencies = [
- "indexmap 2.11.4",
+ "indexmap",
  "toml_datetime 0.7.3",
  "toml_parser",
  "winnow",
@@ -18450,7 +18360,7 @@ dependencies = [
  "icons",
  "itertools 0.14.0",
  "menu",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "settings",
  "smallvec",
@@ -18724,7 +18634,7 @@ dependencies = [
  "rand 0.9.2",
  "regex",
  "rust-embed",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "serde_json_lenient",
@@ -18836,7 +18746,7 @@ name = "vercel"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "strum 0.27.2",
 ]
@@ -18886,7 +18796,7 @@ dependencies = [
  "project_panel",
  "regex",
  "release_channel",
- "schemars 1.0.4",
+ "schemars",
  "search",
  "serde",
  "serde_json",
@@ -19148,7 +19058,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2"
 dependencies = [
  "anyhow",
- "indexmap 2.11.4",
+ "indexmap",
  "serde",
  "serde_derive",
  "serde_json",
@@ -19166,7 +19076,7 @@ dependencies = [
  "anyhow",
  "auditable-serde",
  "flate2",
- "indexmap 2.11.4",
+ "indexmap",
  "serde",
  "serde_derive",
  "serde_json",
@@ -19196,7 +19106,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
 dependencies = [
  "bitflags 2.9.4",
- "indexmap 2.11.4",
+ "indexmap",
  "semver",
 ]
 
@@ -19208,7 +19118,7 @@ checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185"
 dependencies = [
  "bitflags 2.9.4",
  "hashbrown 0.15.5",
- "indexmap 2.11.4",
+ "indexmap",
  "semver",
  "serde",
 ]
@@ -19221,7 +19131,7 @@ checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2"
 dependencies = [
  "bitflags 2.9.4",
  "hashbrown 0.15.5",
- "indexmap 2.11.4",
+ "indexmap",
  "semver",
 ]
 
@@ -19250,7 +19160,7 @@ dependencies = [
  "cfg-if",
  "encoding_rs",
  "hashbrown 0.14.5",
- "indexmap 2.11.4",
+ "indexmap",
  "libc",
  "log",
  "mach2 0.4.3",
@@ -19374,7 +19284,7 @@ dependencies = [
  "cranelift-bitset",
  "cranelift-entity",
  "gimli 0.31.1",
- "indexmap 2.11.4",
+ "indexmap",
  "log",
  "object 0.36.7",
  "postcard",
@@ -19499,7 +19409,7 @@ checksum = "8358319c2dd1e4db79e3c1c5d3a5af84956615343f9f89f4e4996a36816e06e6"
 dependencies = [
  "anyhow",
  "heck 0.5.0",
- "indexmap 2.11.4",
+ "indexmap",
  "wit-parser 0.221.3",
 ]
 
@@ -20667,7 +20577,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3"
 dependencies = [
  "anyhow",
  "heck 0.4.1",
- "indexmap 2.11.4",
+ "indexmap",
  "wasm-metadata 0.201.0",
  "wit-bindgen-core 0.22.0",
  "wit-component 0.201.0",
@@ -20681,7 +20591,7 @@ checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce"
 dependencies = [
  "anyhow",
  "heck 0.5.0",
- "indexmap 2.11.4",
+ "indexmap",
  "prettyplease",
  "syn 2.0.106",
  "wasm-metadata 0.227.1",
@@ -20726,7 +20636,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
 dependencies = [
  "anyhow",
  "bitflags 2.9.4",
- "indexmap 2.11.4",
+ "indexmap",
  "log",
  "serde",
  "serde_derive",
@@ -20745,7 +20655,7 @@ checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676"
 dependencies = [
  "anyhow",
  "bitflags 2.9.4",
- "indexmap 2.11.4",
+ "indexmap",
  "log",
  "serde",
  "serde_derive",
@@ -20764,7 +20674,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6"
 dependencies = [
  "anyhow",
  "id-arena",
- "indexmap 2.11.4",
+ "indexmap",
  "log",
  "semver",
  "serde",
@@ -20782,7 +20692,7 @@ checksum = "896112579ed56b4a538b07a3d16e562d101ff6265c46b515ce0c701eef16b2ac"
 dependencies = [
  "anyhow",
  "id-arena",
- "indexmap 2.11.4",
+ "indexmap",
  "log",
  "semver",
  "serde",
@@ -20800,7 +20710,7 @@ checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11"
 dependencies = [
  "anyhow",
  "id-arena",
- "indexmap 2.11.4",
+ "indexmap",
  "log",
  "semver",
  "serde",
@@ -20850,7 +20760,7 @@ dependencies = [
  "pretty_assertions",
  "project",
  "remote",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "serde_json",
  "session",
@@ -20965,7 +20875,7 @@ name = "x_ai"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "strum 0.27.2",
 ]
@@ -21064,7 +20974,7 @@ dependencies = [
  "cargo_toml",
  "clap",
  "gh-workflow",
- "indexmap 2.11.4",
+ "indexmap",
  "indoc",
  "serde",
  "toml 0.8.23",
@@ -21515,7 +21425,7 @@ name = "zed_actions"
 version = "0.1.0"
 dependencies = [
  "gpui",
- "schemars 1.0.4",
+ "schemars",
  "serde",
  "uuid",
 ]

Cargo.toml 🔗

@@ -641,7 +641,6 @@ serde_json_lenient = { version = "0.2", features = [
 serde_path_to_error = "0.1.17"
 serde_repr = "0.1"
 serde_urlencoded = "0.7"
-serde_with = "3.4.0"
 sha2 = "0.10"
 shellexpand = "2.1.0"
 shlex = "1.3.0"

crates/settings/Cargo.toml 🔗

@@ -34,7 +34,6 @@ serde.workspace = true
 serde_json.workspace = true
 serde_json_lenient.workspace = true
 serde_repr.workspace = true
-serde_with.workspace = true
 settings_json.workspace = true
 settings_macros.workspace = true
 smallvec.workspace = true

crates/settings/src/fallible_options.rs 🔗

@@ -0,0 +1,112 @@
+use std::cell::RefCell;
+
+use serde::Deserialize;
+
+use crate::ParseStatus;
+
+thread_local! {
+    static ERRORS: RefCell<Option<Vec<anyhow::Error>>> = const { RefCell::new(None) };
+}
+
+pub(crate) fn parse_json<'de, T>(json: &'de str) -> (Option<T>, ParseStatus)
+where
+    T: Deserialize<'de>,
+{
+    ERRORS.with_borrow_mut(|errors| {
+        errors.replace(Vec::default());
+    });
+
+    let mut deserializer = serde_json_lenient::Deserializer::from_str(json);
+    let value = T::deserialize(&mut deserializer);
+    let value = match value {
+        Ok(value) => value,
+        Err(error) => {
+            return (
+                None,
+                ParseStatus::Failed {
+                    error: error.to_string(),
+                },
+            );
+        }
+    };
+
+    if let Some(errors) = ERRORS.with_borrow_mut(|errors| errors.take().filter(|e| !e.is_empty())) {
+        let error = errors
+            .into_iter()
+            .map(|e| e.to_string())
+            .flat_map(|e| ["\n".to_owned(), e])
+            .skip(1)
+            .collect::<String>();
+        return (Some(value), ParseStatus::Failed { error });
+    }
+
+    (Some(value), ParseStatus::Success)
+}
+
+pub(crate) fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
+where
+    D: serde::Deserializer<'de>,
+    T: serde::Deserialize<'de> + FallibleOption,
+{
+    match T::deserialize(deserializer) {
+        Ok(value) => Ok(value),
+        Err(e) => ERRORS.with_borrow_mut(|errors| {
+            if let Some(errors) = errors {
+                errors.push(anyhow::anyhow!("{}", e));
+                Ok(Default::default())
+            } else {
+                Err(e)
+            }
+        }),
+    }
+}
+
+pub trait FallibleOption: Default {}
+impl<T> FallibleOption for Option<T> {}
+
+#[cfg(test)]
+mod tests {
+    use serde::Deserialize;
+    use settings_macros::with_fallible_options;
+
+    use crate::ParseStatus;
+
+    #[with_fallible_options]
+    #[derive(Deserialize, Debug, PartialEq)]
+    struct Foo {
+        foo: Option<String>,
+        bar: Option<usize>,
+        baz: Option<bool>,
+    }
+
+    #[test]
+    fn test_fallible() {
+        let input = r#"
+            {"foo": "bar",
+            "bar": "foo",
+            "baz": 3,
+            }
+        "#;
+
+        let (settings, result) = crate::fallible_options::parse_json::<Foo>(&input);
+        assert_eq!(
+            settings.unwrap(),
+            Foo {
+                foo: Some("bar".into()),
+                bar: None,
+                baz: None,
+            }
+        );
+
+        assert!(crate::parse_json_with_comments::<Foo>(&input).is_err());
+
+        let ParseStatus::Failed { error } = result else {
+            panic!("Expected parse to fail")
+        };
+
+        assert_eq!(
+            error,
+            "invalid type: string \"foo\", expected usize at line 3 column 24\ninvalid type: integer `3`, expected a boolean at line 4 column 20".to_string()
+        )
+    }
+}

crates/settings/src/settings.rs 🔗

@@ -1,5 +1,6 @@
 mod base_keymap_setting;
 mod editable_setting_control;
+mod fallible_options;
 mod keymap_file;
 pub mod merge_from;
 mod serde_helper;
@@ -33,7 +34,7 @@ pub use settings_file::*;
 pub use settings_json::*;
 pub use settings_store::{
     InvalidSettingsError, LocalSettingsKind, MigrationStatus, ParseStatus, Settings, SettingsFile,
-    SettingsJsonSchemaParams, SettingsKey, SettingsLocation, SettingsStore,
+    SettingsJsonSchemaParams, SettingsKey, SettingsLocation, SettingsParseResult, SettingsStore,
 };
 
 pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};

crates/settings/src/settings_content.rs 🔗

@@ -23,8 +23,7 @@ use gpui::{App, SharedString};
 use release_channel::ReleaseChannel;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-use settings_macros::MergeFrom;
+use settings_macros::{MergeFrom, with_fallible_options};
 use std::collections::BTreeSet;
 use std::env;
 use std::sync::Arc;
@@ -32,7 +31,7 @@ pub use util::serde::default_true;
 
 use crate::{ActiveSettingsProfileName, merge_from};
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct SettingsContent {
     #[serde(flatten)]
@@ -169,7 +168,7 @@ impl SettingsContent {
     }
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct UserSettingsContent {
     #[serde(flatten)]
@@ -260,7 +259,7 @@ impl strum::VariantNames for BaseKeymapContent {
     ];
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct TitleBarSettingsContent {
     /// Whether to show the branch icon beside branch switcher in the title bar.
@@ -294,7 +293,7 @@ pub struct TitleBarSettingsContent {
 }
 
 /// Configuration of audio in Zed.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct AudioSettingsContent {
     /// Opt into the new audio system.
@@ -338,7 +337,7 @@ pub struct AudioSettingsContent {
 }
 
 /// Control what info is collected by Zed.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug, MergeFrom)]
 pub struct TelemetrySettingsContent {
     /// Send debug info like crash reports.
@@ -360,7 +359,7 @@ impl Default for TelemetrySettingsContent {
     }
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone, MergeFrom)]
 pub struct DebuggerSettingsContent {
     /// Determines the stepping granularity.
@@ -441,7 +440,7 @@ pub enum DockPosition {
 }
 
 /// Settings for slash commands.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct SlashCommandSettings {
     /// Settings for the `/cargo-workspace` slash command.
@@ -449,7 +448,7 @@ pub struct SlashCommandSettings {
 }
 
 /// Settings for the `/cargo-workspace` slash command.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct CargoWorkspaceCommandSettings {
     /// Whether `/cargo-workspace` is enabled.
@@ -457,7 +456,7 @@ pub struct CargoWorkspaceCommandSettings {
 }
 
 /// Configuration of voice calls in Zed.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct CallSettingsContent {
     /// Whether the microphone should be muted when joining a channel or a call.
@@ -471,7 +470,7 @@ pub struct CallSettingsContent {
     pub share_on_join: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct GitPanelSettingsContent {
     /// Whether to show the panel button in the status bar.
@@ -535,7 +534,7 @@ pub enum StatusStyle {
     LabelColor,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
 )]
@@ -543,7 +542,7 @@ pub struct ScrollbarSettings {
     pub show: Option<ShowScrollbar>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct NotificationPanelSettingsContent {
     /// Whether to show the panel button in the status bar.
@@ -561,7 +560,7 @@ pub struct NotificationPanelSettingsContent {
     pub default_width: Option<f32>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct PanelSettingsContent {
     /// Whether to show the panel button in the status bar.
@@ -579,7 +578,7 @@ pub struct PanelSettingsContent {
     pub default_width: Option<f32>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct MessageEditorSettings {
     /// Whether to automatically replace emoji shortcodes with emoji characters.
@@ -589,7 +588,7 @@ pub struct MessageEditorSettings {
     pub auto_replace_emoji_shortcode: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct FileFinderSettingsContent {
     /// Whether to show file icons in the file finder.
@@ -664,7 +663,7 @@ pub enum FileFinderWidthContent {
     Full,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug, JsonSchema, MergeFrom)]
 pub struct VimSettingsContent {
     pub default_mode: Option<ModeContent>,
@@ -697,7 +696,7 @@ pub enum UseSystemClipboard {
 }
 
 /// The settings for cursor shape.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 pub struct CursorShapeSettings {
     /// Cursor shape for the normal mode.
@@ -719,7 +718,7 @@ pub struct CursorShapeSettings {
 }
 
 /// Settings specific to journaling
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct JournalSettingsContent {
     /// The path of the directory where journal entries are stored.
@@ -740,7 +739,7 @@ pub enum HourFormat {
     Hour24,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct OutlinePanelSettingsContent {
     /// Whether to show the outline panel button in the status bar.
@@ -835,7 +834,7 @@ pub enum ShowIndentGuides {
     Never,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
 )]
@@ -853,7 +852,7 @@ pub enum LineIndicatorFormat {
 }
 
 /// The settings for the image viewer.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, Default, PartialEq)]
 pub struct ImageViewerSettingsContent {
     /// The unit to use for displaying image file sizes.
@@ -862,7 +861,7 @@ pub struct ImageViewerSettingsContent {
     pub unit: Option<ImageFileSizeUnit>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Clone,
     Copy,
@@ -885,7 +884,7 @@ pub enum ImageFileSizeUnit {
     Decimal,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct RemoteSettingsContent {
     pub ssh_connections: Option<Vec<SshConnection>>,
@@ -893,7 +892,7 @@ pub struct RemoteSettingsContent {
     pub read_ssh_config: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct SshConnection {
     pub host: SharedString,
@@ -922,7 +921,7 @@ pub struct WslConnection {
     pub projects: BTreeSet<SshProject>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Clone, Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema,
 )]
@@ -930,19 +929,17 @@ pub struct SshProject {
     pub paths: Vec<String>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, MergeFrom)]
 pub struct SshPortForwardOption {
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub local_host: Option<String>,
     pub local_port: u16,
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub remote_host: Option<String>,
     pub remote_port: u16,
 }
 
 /// Settings for configuring REPL display and behavior.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ReplSettingsContent {
     /// Maximum number of lines to keep in REPL's scrollback buffer.

crates/settings/src/settings_content/agent.rs 🔗

@@ -2,13 +2,12 @@ use collections::{HashMap, IndexMap};
 use gpui::SharedString;
 use schemars::{JsonSchema, json_schema};
 use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-use settings_macros::MergeFrom;
+use settings_macros::{MergeFrom, with_fallible_options};
 use std::{borrow::Cow, path::PathBuf, sync::Arc};
 
 use crate::DockPosition;
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
 pub struct AgentSettingsContent {
     /// Whether the Agent is enabled.
@@ -166,7 +165,7 @@ impl AgentSettingsContent {
     }
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct AgentProfileContent {
     pub name: Arc<str>,
@@ -180,7 +179,7 @@ pub struct AgentProfileContent {
     pub default_model: Option<LanguageModelSelection>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ContextServerPresetContent {
     pub tools: IndexMap<Arc<str>, bool>,
@@ -215,7 +214,7 @@ pub enum NotifyWhenAgentWaiting {
     Never,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct LanguageModelSelection {
     pub provider: LanguageModelProviderSetting,
@@ -231,7 +230,7 @@ pub enum CompletionMode {
     Burn,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct LanguageModelParameters {
     pub provider: Option<LanguageModelProviderSetting>,
@@ -290,7 +289,7 @@ impl From<&str> for LanguageModelProviderSetting {
     }
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)]
 pub struct AllAgentServersSettings {
     pub gemini: Option<BuiltinAgentServerSettings>,
@@ -302,7 +301,7 @@ pub struct AllAgentServersSettings {
     pub custom: HashMap<SharedString, CustomAgentServerSettings>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct BuiltinAgentServerSettings {
     /// Absolute path to a binary to be used when launching this agent.
@@ -340,7 +339,7 @@ pub struct BuiltinAgentServerSettings {
     pub default_model: Option<String>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
 #[serde(tag = "type", rename_all = "snake_case")]
 pub enum CustomAgentServerSettings {

crates/settings/src/settings_content/editor.rs 🔗

@@ -4,14 +4,13 @@ use std::num;
 use collections::HashMap;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-use settings_macros::MergeFrom;
+use settings_macros::{MergeFrom, with_fallible_options};
 
 use crate::{
     DelayMs, DiagnosticSeverityContent, ShowScrollbar, serialize_f32_with_two_decimal_places,
 };
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct EditorSettingsContent {
     /// Whether the cursor blinks in the editor.
@@ -254,7 +253,7 @@ impl RelativeLineNumbers {
 }
 
 // Toolbar related settings
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct ToolbarContent {
     /// Whether to display breadcrumbs in the editor toolbar.
@@ -281,7 +280,7 @@ pub struct ToolbarContent {
 }
 
 /// Scrollbar related settings
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)]
 pub struct ScrollbarContent {
     /// When to show the scrollbar in the editor.
@@ -317,7 +316,7 @@ pub struct ScrollbarContent {
 }
 
 /// Sticky scroll related settings
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct StickyScrollContent {
     /// Whether sticky scroll is enabled.
@@ -327,7 +326,7 @@ pub struct StickyScrollContent {
 }
 
 /// Minimap related settings
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct MinimapContent {
     /// When to show the minimap in the editor.
@@ -362,7 +361,7 @@ pub struct MinimapContent {
 }
 
 /// Forcefully enable or disable the scrollbar for each axis
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)]
 pub struct ScrollbarAxesContent {
     /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings.
@@ -377,7 +376,7 @@ pub struct ScrollbarAxesContent {
 }
 
 /// Gutter related settings
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct GutterContent {
     /// Whether to show line numbers in the gutter.
@@ -754,7 +753,7 @@ pub enum SnippetSortOrder {
 }
 
 /// Default options for buffer and project search items.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct SearchSettingsContent {
     /// Whether to show the project search button in the status bar.
@@ -771,7 +770,7 @@ pub struct SearchSettingsContent {
     pub center_on_match: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct JupyterContent {
@@ -787,7 +786,7 @@ pub struct JupyterContent {
 }
 
 /// Whether to allow drag and drop text selection in buffer.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct DragAndDropSelectionContent {
     /// When true, enables drag and drop text selection in buffer.

crates/settings/src/settings_content/extension.rs 🔗

@@ -3,10 +3,9 @@ use std::sync::Arc;
 use collections::HashMap;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-use settings_macros::MergeFrom;
+use settings_macros::{MergeFrom, with_fallible_options};
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ExtensionSettingsContent {
     /// The extensions that should be automatically installed by Zed.
@@ -20,7 +19,6 @@ pub struct ExtensionSettingsContent {
     #[serde(default)]
     pub auto_update_extensions: HashMap<Arc<str>, bool>,
     /// The capabilities granted to extensions.
-    #[serde(default)]
     pub granted_extension_capabilities: Option<Vec<ExtensionCapabilityContent>>,
 }
 

crates/settings/src/settings_content/language.rs 🔗

@@ -4,20 +4,17 @@ use collections::{HashMap, HashSet};
 use gpui::{Modifiers, SharedString};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize, de::Error as _};
-use serde_with::skip_serializing_none;
-use settings_macros::MergeFrom;
+use settings_macros::{MergeFrom, with_fallible_options};
 use std::sync::Arc;
 
 use crate::{ExtendingVec, merge_from};
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
 pub struct AllLanguageSettingsContent {
     /// The settings for enabling/disabling features.
-    #[serde(default)]
     pub features: Option<FeaturesContent>,
     /// The edit prediction settings.
-    #[serde(default)]
     pub edit_predictions: Option<EditPredictionSettingsContent>,
     /// The default language settings.
     #[serde(flatten)]
@@ -59,7 +56,7 @@ impl merge_from::MergeFrom for AllLanguageSettingsContent {
 }
 
 /// The settings for enabling/disabling features.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct FeaturesContent {
@@ -134,7 +131,7 @@ impl EditPredictionProvider {
 }
 
 /// The contents of the edit prediction settings.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct EditPredictionSettingsContent {
     /// A list of globs representing files that edit predictions should be disabled for.
@@ -153,7 +150,7 @@ pub struct EditPredictionSettingsContent {
     pub enabled_in_text_threads: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct CopilotSettingsContent {
     /// HTTP/HTTPS proxy to use for Copilot.
@@ -246,7 +243,7 @@ pub enum SoftWrap {
 }
 
 /// The settings for a particular language.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct LanguageSettingsContent {
     /// How many columns a tab should occupy.
@@ -451,7 +448,7 @@ pub enum ShowWhitespaceSetting {
     Trailing,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 pub struct WhitespaceMapContent {
     pub space: Option<char>,
@@ -483,7 +480,7 @@ pub enum RewrapBehavior {
     Anywhere,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct JsxTagAutoCloseSettingsContent {
     /// Enables or disables auto-closing of JSX tags.
@@ -491,7 +488,7 @@ pub struct JsxTagAutoCloseSettingsContent {
 }
 
 /// The settings for inlay hints.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct InlayHintSettingsContent {
     /// Global switch to toggle hints on and off.
@@ -573,7 +570,7 @@ impl InlayHintKind {
 }
 
 /// Controls how completions are processed for this language.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)]
 #[serde(rename_all = "snake_case")]
 pub struct CompletionSettingsContent {
@@ -658,7 +655,7 @@ pub enum WordsCompletionMode {
 /// Allows to enable/disable formatting with Prettier
 /// and configure default Prettier, used when no project-level Prettier installation is found.
 /// Prettier formatting is disabled by default.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct PrettierSettingsContent {
     /// Enables or disables formatting with Prettier for a given language.
@@ -812,7 +809,7 @@ struct LanguageServerSpecifierContent {
 }
 
 /// The settings for indent guides.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct IndentGuideSettingsContent {
     /// Whether to display indent guides in the editor.
@@ -838,7 +835,7 @@ pub struct IndentGuideSettingsContent {
 }
 
 /// The task settings for a particular language.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, JsonSchema, MergeFrom)]
 pub struct LanguageTaskSettingsContent {
     /// Extra task variables to set for a particular language.
@@ -855,7 +852,7 @@ pub struct LanguageTaskSettingsContent {
 }
 
 /// Map from language name to settings.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct LanguageToSettingsMap(pub HashMap<SharedString, LanguageSettingsContent>);
 
@@ -911,6 +908,9 @@ pub enum IndentGuideBackgroundColoring {
 
 #[cfg(test)]
 mod test {
+
+    use crate::{ParseStatus, fallible_options};
+
     use super::*;
 
     #[test]
@@ -970,8 +970,8 @@ mod test {
     #[test]
     fn test_formatter_deserialization_invalid() {
         let raw_auto = "{\"formatter\": {}}";
-        let result: Result<LanguageSettingsContent, _> = serde_json::from_str(raw_auto);
-        assert!(result.is_err());
+        let (_, result) = fallible_options::parse_json::<LanguageSettingsContent>(raw_auto);
+        assert!(matches!(result, ParseStatus::Failed { .. }));
     }
 
     #[test]

crates/settings/src/settings_content/language_model.rs 🔗

@@ -1,12 +1,11 @@
 use collections::HashMap;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-use settings_macros::MergeFrom;
+use settings_macros::{MergeFrom, with_fallible_options};
 
 use std::sync::Arc;
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct AllLanguageModelSettingsContent {
     pub anthropic: Option<AnthropicSettingsContent>,
@@ -25,14 +24,14 @@ pub struct AllLanguageModelSettingsContent {
     pub zed_dot_dev: Option<ZedDotDevSettingsContent>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct AnthropicSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<AnthropicAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct AnthropicAvailableModel {
     /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc
@@ -54,7 +53,7 @@ pub struct AnthropicAvailableModel {
     pub mode: Option<ModelMode>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct AmazonBedrockSettingsContent {
     pub available_models: Option<Vec<BedrockAvailableModel>>,
@@ -64,7 +63,7 @@ pub struct AmazonBedrockSettingsContent {
     pub authentication_method: Option<BedrockAuthMethodContent>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct BedrockAvailableModel {
     pub name: String,
@@ -88,14 +87,14 @@ pub enum BedrockAuthMethodContent {
     Automatic,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct OllamaSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<OllamaAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OllamaAvailableModel {
     /// The model name in the Ollama API (e.g. "llama3.2:latest")
@@ -136,14 +135,14 @@ impl Default for KeepAlive {
     }
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct LmStudioSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<LmStudioAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct LmStudioAvailableModel {
     pub name: String,
@@ -153,14 +152,14 @@ pub struct LmStudioAvailableModel {
     pub supports_images: bool,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct DeepseekSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<DeepseekAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct DeepseekAvailableModel {
     pub name: String,
@@ -169,14 +168,14 @@ pub struct DeepseekAvailableModel {
     pub max_output_tokens: Option<u64>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct MistralSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<MistralAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct MistralAvailableModel {
     pub name: String,
@@ -189,14 +188,14 @@ pub struct MistralAvailableModel {
     pub supports_thinking: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct OpenAiSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<OpenAiAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OpenAiAvailableModel {
     pub name: String,
@@ -216,14 +215,14 @@ pub enum OpenAiReasoningEffort {
     High,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct OpenAiCompatibleSettingsContent {
     pub api_url: String,
     pub available_models: Vec<OpenAiCompatibleAvailableModel>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OpenAiCompatibleAvailableModel {
     pub name: String,
@@ -235,7 +234,7 @@ pub struct OpenAiCompatibleAvailableModel {
     pub capabilities: OpenAiCompatibleModelCapabilities,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OpenAiCompatibleModelCapabilities {
     pub tools: bool,
@@ -255,14 +254,14 @@ impl Default for OpenAiCompatibleModelCapabilities {
     }
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct VercelSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<VercelAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct VercelAvailableModel {
     pub name: String,
@@ -272,14 +271,14 @@ pub struct VercelAvailableModel {
     pub max_completion_tokens: Option<u64>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct GoogleSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<GoogleAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct GoogleAvailableModel {
     pub name: String,
@@ -288,14 +287,14 @@ pub struct GoogleAvailableModel {
     pub mode: Option<ModelMode>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct XAiSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<XaiAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct XaiAvailableModel {
     pub name: String,
@@ -308,13 +307,13 @@ pub struct XaiAvailableModel {
     pub parallel_tool_calls: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct ZedDotDevSettingsContent {
     pub available_models: Option<Vec<ZedDotDevAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ZedDotDevAvailableModel {
     /// The provider of the language model.
@@ -351,14 +350,14 @@ pub enum ZedDotDevAvailableProvider {
     Google,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)]
 pub struct OpenRouterSettingsContent {
     pub api_url: Option<String>,
     pub available_models: Option<Vec<OpenRouterAvailableModel>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OpenRouterAvailableModel {
     pub name: String,
@@ -372,7 +371,7 @@ pub struct OpenRouterAvailableModel {
     pub provider: Option<OpenRouterProvider>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct OpenRouterProvider {
     order: Option<Vec<String>>,
@@ -401,7 +400,7 @@ fn default_true() -> bool {
 }
 
 /// Configuration for caching language model messages.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct LanguageModelCacheConfiguration {
     pub max_cache_anchors: usize,

crates/settings/src/settings_content/project.rs 🔗

@@ -3,8 +3,7 @@ use std::{path::PathBuf, sync::Arc};
 use collections::{BTreeMap, HashMap};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-use settings_macros::MergeFrom;
+use settings_macros::{MergeFrom, with_fallible_options};
 use util::serde::default_true;
 
 use crate::{
@@ -12,7 +11,7 @@ use crate::{
     SlashCommandSettings,
 };
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ProjectSettingsContent {
     #[serde(flatten)]
@@ -32,7 +31,6 @@ pub struct ProjectSettingsContent {
     #[serde(default)]
     pub lsp: HashMap<Arc<str>, LspSettings>,
 
-    #[serde(default)]
     pub terminal: Option<ProjectTerminalSettingsContent>,
 
     /// Configuration for Debugger-related features
@@ -53,15 +51,13 @@ pub struct ProjectSettingsContent {
     pub git_hosting_providers: Option<ExtendingVec<GitHostingProviderConfig>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct WorktreeSettingsContent {
     /// The displayed name of this project. If not set or null, the root directory name
     /// will be displayed.
     ///
     /// Default: null
-    #[serde(default)]
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub project_name: Option<String>,
 
     /// Whether to prevent this project from being shared in public channels.
@@ -103,7 +99,7 @@ pub struct WorktreeSettingsContent {
     pub hidden_files: Option<Vec<String>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash)]
 #[serde(rename_all = "snake_case")]
 pub struct LspSettings {
@@ -140,7 +136,7 @@ impl Default for LspSettings {
     }
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash,
 )]
@@ -151,7 +147,7 @@ pub struct BinarySettings {
     pub ignore_system_version: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash,
 )]
@@ -161,7 +157,7 @@ pub struct FetchSettings {
 }
 
 /// Common language server settings.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct GlobalLspSettingsContent {
     /// Whether to show the LSP servers button in the status bar.
@@ -170,18 +166,16 @@ pub struct GlobalLspSettingsContent {
     pub button: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct DapSettingsContent {
     pub binary: Option<String>,
-    #[serde(default)]
     pub args: Option<Vec<String>>,
-    #[serde(default)]
     pub env: Option<HashMap<String, String>>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Default, Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema, MergeFrom,
 )]
@@ -249,7 +243,7 @@ impl ContextServerSettingsContent {
     }
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom)]
 pub struct ContextServerCommand {
     #[serde(rename = "command")]
@@ -285,7 +279,7 @@ impl std::fmt::Debug for ContextServerCommand {
     }
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct GitSettings {
     /// Whether or not to show the git gutter.
@@ -339,7 +333,7 @@ pub enum GitGutterSetting {
     Hide,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct InlineBlameSettings {
@@ -368,7 +362,7 @@ pub struct InlineBlameSettings {
     pub show_commit_summary: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct BlameSettings {
@@ -378,7 +372,7 @@ pub struct BlameSettings {
     pub show_avatar: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct BranchPickerSettingsContent {
@@ -410,6 +404,7 @@ pub enum GitHunkStyleSetting {
     UnstagedHollow,
 }
 
+#[with_fallible_options]
 #[derive(
     Copy,
     Clone,
@@ -432,7 +427,7 @@ pub enum GitPathStyle {
     FilePathFirst,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct DiagnosticsSettingsContent {
     /// Whether to show the project diagnostics button in the status bar.
@@ -448,7 +443,7 @@ pub struct DiagnosticsSettingsContent {
     pub inline: Option<InlineDiagnosticsSettingsContent>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
 )]
@@ -464,7 +459,7 @@ pub struct LspPullDiagnosticsSettingsContent {
     pub debounce_ms: Option<DelayMs>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Eq,
 )]
@@ -493,7 +488,7 @@ pub struct InlineDiagnosticsSettingsContent {
     pub max_severity: Option<DiagnosticSeverityContent>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct NodeBinarySettings {
     /// The path to the Node binary.
@@ -541,7 +536,7 @@ pub enum DiagnosticSeverityContent {
 }
 
 /// A custom Git hosting provider.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct GitHostingProviderConfig {
     /// The type of the provider.

crates/settings/src/settings_content/terminal.rs 🔗

@@ -4,8 +4,7 @@ use collections::HashMap;
 use gpui::{AbsoluteLength, FontFeatures, SharedString, px};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-use settings_macros::MergeFrom;
+use settings_macros::{MergeFrom, with_fallible_options};
 
 use crate::FontFamilyName;
 
@@ -32,7 +31,7 @@ pub struct ProjectTerminalSettingsContent {
     pub detect_venv: Option<VenvSettings>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct TerminalSettingsContent {
     #[serde(flatten)]
@@ -201,7 +200,7 @@ pub enum WorkingDirectory {
     Always { directory: String },
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
 )]
@@ -339,7 +338,7 @@ pub enum AlternateScroll {
 }
 
 // Toolbar related settings
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 pub struct TerminalToolbarContent {
     /// Whether to display the terminal title in breadcrumbs inside the terminal pane.
@@ -386,7 +385,7 @@ pub enum VenvSettings {
         conda_manager: Option<CondaManager>,
     },
 }
-#[skip_serializing_none]
+#[with_fallible_options]
 pub struct VenvSettingsContent<'a> {
     pub activate_script: ActivateScript,
     pub venv_name: &'a str,

crates/settings/src/settings_content/theme.rs 🔗

@@ -4,90 +4,72 @@ use schemars::{JsonSchema, JsonSchema_repr};
 use serde::{Deserialize, Deserializer, Serialize};
 use serde_json::Value;
 use serde_repr::{Deserialize_repr, Serialize_repr};
-use settings_macros::MergeFrom;
+use settings_macros::{MergeFrom, with_fallible_options};
 use std::{fmt::Display, sync::Arc};
 
-use serde_with::skip_serializing_none;
-
 use crate::serialize_f32_with_two_decimal_places;
 
 /// Settings for rendering text in UI and text buffers.
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ThemeSettingsContent {
     /// The default font size for text in the UI.
-    #[serde(default)]
     #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub ui_font_size: Option<f32>,
     /// The name of a font to use for rendering in the UI.
-    #[serde(default)]
     pub ui_font_family: Option<FontFamilyName>,
     /// The font fallbacks to use for rendering in the UI.
-    #[serde(default)]
     #[schemars(default = "default_font_fallbacks")]
     #[schemars(extend("uniqueItems" = true))]
     pub ui_font_fallbacks: Option<Vec<FontFamilyName>>,
     /// The OpenType features to enable for text in the UI.
-    #[serde(default)]
     #[schemars(default = "default_font_features")]
     pub ui_font_features: Option<FontFeatures>,
     /// The weight of the UI font in CSS units from 100 to 900.
-    #[serde(default)]
     #[schemars(default = "default_buffer_font_weight")]
     pub ui_font_weight: Option<FontWeight>,
     /// The name of a font to use for rendering in text buffers.
-    #[serde(default)]
     pub buffer_font_family: Option<FontFamilyName>,
     /// The font fallbacks to use for rendering in text buffers.
-    #[serde(default)]
     #[schemars(extend("uniqueItems" = true))]
     pub buffer_font_fallbacks: Option<Vec<FontFamilyName>>,
     /// The default font size for rendering in text buffers.
-    #[serde(default)]
     #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub buffer_font_size: Option<f32>,
     /// The weight of the editor font in CSS units from 100 to 900.
-    #[serde(default)]
     #[schemars(default = "default_buffer_font_weight")]
     pub buffer_font_weight: Option<FontWeight>,
     /// The buffer's line height.
-    #[serde(default)]
     pub buffer_line_height: Option<BufferLineHeight>,
     /// The OpenType features to enable for rendering in text buffers.
-    #[serde(default)]
     #[schemars(default = "default_font_features")]
     pub buffer_font_features: Option<FontFeatures>,
     /// The font size for agent responses in the agent panel. Falls back to the UI font size if unset.
-    #[serde(default)]
     #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub agent_ui_font_size: Option<f32>,
     /// The font size for user messages in the agent panel.
-    #[serde(default)]
     #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
     pub agent_buffer_font_size: Option<f32>,
     /// The name of the Zed theme to use.
-    #[serde(default)]
     pub theme: Option<ThemeSelection>,
     /// The name of the icon theme to use.
-    #[serde(default)]
     pub icon_theme: Option<IconThemeSelection>,
 
     /// UNSTABLE: Expect many elements to be broken.
     ///
     // Controls the density of the UI.
-    #[serde(rename = "unstable.ui_density", default)]
+    #[serde(rename = "unstable.ui_density")]
     pub ui_density: Option<UiDensity>,
 
     /// How much to fade out unused code.
-    #[serde(default)]
     #[schemars(range(min = 0.0, max = 0.9))]
     pub unnecessary_code_fade: Option<CodeFade>,
 
     /// EXPERIMENTAL: Overrides for the current theme.
     ///
     /// These values will override the ones on the current theme specified in `theme`.
-    #[serde(rename = "experimental.theme_overrides", default)]
+    #[serde(rename = "experimental.theme_overrides")]
     pub experimental_theme_overrides: Option<ThemeStyleContent>,
 
     /// Overrides per theme
@@ -270,7 +252,7 @@ impl UiDensity {
 }
 
 /// Font family name.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(transparent)]
 pub struct FontFamilyName(pub Arc<str>);
@@ -345,11 +327,11 @@ where
 }
 
 /// The content of a serialized theme.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(default)]
 pub struct ThemeStyleContent {
-    #[serde(default, rename = "background.appearance")]
+    #[serde(rename = "background.appearance")]
     pub window_background_appearance: Option<WindowBackgroundContent>,
 
     #[serde(default)]
@@ -380,18 +362,18 @@ pub struct PlayerColorContent {
 }
 
 /// Theme name.
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(transparent)]
 pub struct ThemeName(pub Arc<str>);
 
 /// Icon Theme Name
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
 #[serde(transparent)]
 pub struct IconThemeName(pub Arc<str>);
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(default)]
 pub struct ThemeColorsContent {
@@ -925,19 +907,27 @@ pub struct ThemeColorsContent {
     pub vim_mode_text: Option<String>,
 }
 
-#[skip_serializing_none]
 #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(default)]
 pub struct HighlightStyleContent {
     pub color: Option<String>,
 
-    #[serde(deserialize_with = "treat_error_as_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "treat_error_as_none"
+    )]
     pub background_color: Option<String>,
 
-    #[serde(deserialize_with = "treat_error_as_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "treat_error_as_none"
+    )]
     pub font_style: Option<FontStyleContent>,
 
-    #[serde(deserialize_with = "treat_error_as_none")]
+    #[serde(
+        skip_serializing_if = "Option::is_none",
+        deserialize_with = "treat_error_as_none"
+    )]
     pub font_weight: Option<FontWeightContent>,
 }
 
@@ -959,7 +949,7 @@ where
     Ok(T::deserialize(value).ok())
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
 #[serde(default)]
 pub struct StatusColorsContent {

crates/settings/src/settings_content/workspace.rs 🔗

@@ -3,15 +3,14 @@ use std::num::NonZeroUsize;
 use collections::HashMap;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use serde_with::skip_serializing_none;
-use settings_macros::MergeFrom;
+use settings_macros::{MergeFrom, with_fallible_options};
 
 use crate::{
     CenteredPaddingSettings, DelayMs, DockPosition, DockSide, InactiveOpacity,
     ScrollbarSettingsContent, ShowIndentGuides, serialize_optional_f32_with_two_decimal_places,
 };
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct WorkspaceSettingsContent {
     /// Active pane styling settings.
@@ -112,7 +111,7 @@ pub struct WorkspaceSettingsContent {
     pub zoomed_padding: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct ItemSettingsContent {
     /// Whether to show the Git file status on a tab item.
@@ -142,7 +141,7 @@ pub struct ItemSettingsContent {
     pub show_close_button: Option<ShowCloseButton>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
 pub struct PreviewTabsSettingsContent {
     /// Whether to show opened editors as preview tabs.
@@ -244,7 +243,7 @@ pub enum ActivateOnClose {
     LeftNeighbour,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
 #[serde(rename_all = "snake_case")]
 pub struct ActivePaneModifiers {
@@ -350,7 +349,7 @@ pub enum RestoreOnStartupBehavior {
     LastSession,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)]
 pub struct TabBarSettingsContent {
     /// Whether or not to show the tab bar in the editor.
@@ -367,13 +366,13 @@ pub struct TabBarSettingsContent {
     pub show_tab_bar_buttons: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq, Eq)]
 pub struct StatusBarSettingsContent {
     /// Whether to show the status bar.
     ///
     /// Default: true
-    #[serde(rename = "experimental.show", default)]
+    #[serde(rename = "experimental.show")]
     pub show: Option<bool>,
     /// Whether to display the active language button in the status bar.
     ///
@@ -465,7 +464,7 @@ pub enum PaneSplitDirectionVertical {
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)]
 #[serde(rename_all = "snake_case")]
-#[skip_serializing_none]
+#[with_fallible_options]
 pub struct CenteredLayoutSettings {
     /// The relative width of the left padding of the central pane from the
     /// workspace when the centered layout is used.
@@ -510,7 +509,7 @@ impl OnLastWindowClosed {
     }
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct ProjectPanelAutoOpenSettings {
     /// Whether to automatically open newly created files in the editor.
@@ -527,7 +526,7 @@ pub struct ProjectPanelAutoOpenSettings {
     pub on_drop: Option<bool>,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct ProjectPanelSettingsContent {
     /// Whether to show the project panel button in the status bar.
@@ -663,7 +662,7 @@ pub enum ProjectPanelSortMode {
     FilesFirst,
 }
 
-#[skip_serializing_none]
+#[with_fallible_options]
 #[derive(
     Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default,
 )]

crates/settings/src/settings_store.rs 🔗

@@ -32,7 +32,7 @@ pub type EditorconfigProperties = ec4rs::Properties;
 
 use crate::{
     ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent,
-    LanguageToSettingsMap, ThemeName, VsCodeSettings, WorktreeId,
+    LanguageToSettingsMap, ThemeName, VsCodeSettings, WorktreeId, fallible_options,
     merge_from::MergeFrom,
     settings_content::{
         ExtensionsSettingsContent, ProjectSettingsContent, SettingsContent, UserSettingsContent,
@@ -666,44 +666,31 @@ impl SettingsStore {
         file: SettingsFile,
     ) -> (Option<SettingsContentType>, SettingsParseResult) {
         let mut migration_status = MigrationStatus::NotNeeded;
-        let settings: SettingsContentType = if user_settings_content.is_empty() {
-            parse_json_with_comments("{}").expect("Empty settings should always be valid")
+        let (settings, parse_status) = if user_settings_content.is_empty() {
+            fallible_options::parse_json("{}")
         } else {
             let migration_res = migrator::migrate_settings(user_settings_content);
-            let content = match &migration_res {
-                Ok(Some(content)) => content,
-                Ok(None) => user_settings_content,
-                Err(_) => user_settings_content,
-            };
-            let parse_result = parse_json_with_comments(content);
-            migration_status = match migration_res {
+            migration_status = match &migration_res {
                 Ok(Some(_)) => MigrationStatus::Succeeded,
                 Ok(None) => MigrationStatus::NotNeeded,
                 Err(err) => MigrationStatus::Failed {
                     error: err.to_string(),
                 },
             };
-            match parse_result {
-                Ok(settings) => settings,
-                Err(err) => {
-                    let result = SettingsParseResult {
-                        parse_status: ParseStatus::Failed {
-                            error: err.to_string(),
-                        },
-                        migration_status,
-                    };
-                    self.file_errors.insert(file, result.clone());
-                    return (None, result);
-                }
-            }
+            let content = match &migration_res {
+                Ok(Some(content)) => content,
+                Ok(None) => user_settings_content,
+                Err(_) => user_settings_content,
+            };
+            fallible_options::parse_json(content)
         };
 
         let result = SettingsParseResult {
-            parse_status: ParseStatus::Success,
+            parse_status,
             migration_status,
         };
         self.file_errors.insert(file, result.clone());
-        return (Some(settings), result);
+        return (settings, result);
     }
 
     pub fn error_for_file(&self, file: SettingsFile) -> Option<SettingsParseResult> {

crates/settings_macros/src/settings_macros.rs 🔗

@@ -1,6 +1,9 @@
 use proc_macro::TokenStream;
+
 use quote::quote;
-use syn::{Data, DeriveInput, Fields, parse_macro_input};
+use syn::{
+    Data, DeriveInput, Field, Fields, ItemEnum, ItemStruct, Type, parse_macro_input, parse_quote,
+};
 
 /// Derives the `MergeFrom` trait for a struct.
 ///
@@ -100,3 +103,50 @@ pub fn derive_register_setting(input: TokenStream) -> TokenStream {
     }
     .into()
 }
+
+// Adds serde attributes to each field with type Option<T>:
+// #serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "settings::deserialize_fallible")
+#[proc_macro_attribute]
+pub fn with_fallible_options(_args: TokenStream, input: TokenStream) -> TokenStream {
+    fn apply_on_fields(fields: &mut Fields) {
+        match fields {
+            Fields::Unit => {}
+            Fields::Named(fields) => {
+                for field in &mut fields.named {
+                    add_if_option(field)
+                }
+            }
+            Fields::Unnamed(fields) => {
+                for field in &mut fields.unnamed {
+                    add_if_option(field)
+                }
+            }
+        }
+    }
+
+    fn add_if_option(field: &mut Field) {
+        match &field.ty {
+            Type::Path(syn::TypePath { qself: None, path })
+                if path.leading_colon.is_none()
+                    && path.segments.len() == 1
+                    && path.segments[0].ident == "Option" => {}
+            _ => return,
+        }
+        let attr = parse_quote!(
+            #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with="crate::fallible_options::deserialize")]
+        );
+        field.attrs.push(attr);
+    }
+
+    if let Ok(mut input) = syn::parse::<ItemStruct>(input.clone()) {
+        apply_on_fields(&mut input.fields);
+        quote!(#input).into()
+    } else if let Ok(mut input) = syn::parse::<ItemEnum>(input) {
+        for variant in &mut input.variants {
+            apply_on_fields(&mut variant.fields);
+        }
+        quote!(#input).into()
+    } else {
+        panic!("with_fallible_options can only be applied to struct or enum definitions.");
+    }
+}

crates/zed/src/main.rs 🔗

@@ -50,8 +50,8 @@ use workspace::{
 use zed::{
     OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options,
     derive_paths_with_position, edit_prediction_registry, handle_cli_connection,
-    handle_keymap_file_changes, handle_settings_changed, handle_settings_file_changes,
-    initialize_workspace, open_paths_with_positions,
+    handle_keymap_file_changes, handle_settings_file_changes, initialize_workspace,
+    open_paths_with_positions,
 };
 
 use crate::zed::{OpenRequestKind, eager_load_active_theme_and_icon_theme};
@@ -411,12 +411,7 @@ pub fn main() {
         }
         settings::init(cx);
         zlog_settings::init(cx);
-        handle_settings_file_changes(
-            user_settings_file_rx,
-            global_settings_file_rx,
-            cx,
-            handle_settings_changed,
-        );
+        handle_settings_file_changes(user_settings_file_rx, global_settings_file_rx, cx);
         handle_keymap_file_changes(user_keymap_file_rx, cx);
 
         let user_agent = format!(

crates/zed/src/zed.rs 🔗

@@ -58,7 +58,7 @@ use rope::Rope;
 use search::project_search::ProjectSearchBar;
 use settings::{
     BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile,
-    KeymapFileLoadResult, Settings, SettingsStore, VIM_KEYMAP_PATH,
+    KeymapFileLoadResult, MigrationStatus, Settings, SettingsStore, VIM_KEYMAP_PATH,
     initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
     update_settings_file,
 };
@@ -1363,44 +1363,63 @@ fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Contex
         .detach();
 }
 
-pub fn handle_settings_file_changes(
-    mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
-    mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
-    cx: &mut App,
-    settings_changed: impl Fn(Option<anyhow::Error>, &mut App) + 'static,
-) {
-    MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
+fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool, cx: &mut App) {
+    if let settings::ParseStatus::Failed { error: err } = &result.parse_status {
+        let settings_type = if is_user { "user" } else { "global" };
+        log::error!("Failed to load {} settings: {err}", settings_type);
+    }
 
-    // Helper function to process settings content
-    let process_settings = move |content: String,
-                                 is_user: bool,
-                                 store: &mut SettingsStore,
-                                 cx: &mut App|
-          -> bool {
-        let result = if is_user {
-            store.set_user_settings(&content, cx)
-        } else {
-            store.set_global_settings(&content, cx)
-        };
+    let error = match result.parse_status {
+        settings::ParseStatus::Failed { error } => Some(anyhow::format_err!(error)),
+        settings::ParseStatus::Success => None,
+    };
+    struct SettingsParseErrorNotification;
+    let id = NotificationId::unique::<SettingsParseErrorNotification>();
 
-        let id = NotificationId::Named("failed-to-migrate-settings".into());
-        // Apply migrations to both user and global settings
-        let content_migrated = match result.migration_status {
-            settings::MigrationStatus::Succeeded => {
-                dismiss_app_notification(&id, cx);
-                true
-            }
-            settings::MigrationStatus::NotNeeded => {
-                dismiss_app_notification(&id, cx);
+    let showed_parse_error = match error {
+        Some(error) => {
+            if let Some(InvalidSettingsError::LocalSettings { .. }) =
+                error.downcast_ref::<InvalidSettingsError>()
+            {
                 false
+                // Local settings errors are displayed by the projects
+            } else {
+                show_app_notification(id, cx, move |cx| {
+                    cx.new(|cx| {
+                        MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
+                            .primary_message("Open Settings File")
+                            .primary_icon(IconName::Settings)
+                            .primary_on_click(|window, cx| {
+                                window.dispatch_action(
+                                    zed_actions::OpenSettingsFile.boxed_clone(),
+                                    cx,
+                                );
+                                cx.emit(DismissEvent);
+                            })
+                    })
+                });
+                true
             }
-            settings::MigrationStatus::Failed { error: err } => {
+        }
+        None => {
+            dismiss_app_notification(&id, cx);
+            false
+        }
+    };
+    let id = NotificationId::Named("failed-to-migrate-settings".into());
+
+    match result.migration_status {
+        settings::MigrationStatus::Succeeded | settings::MigrationStatus::NotNeeded => {
+            dismiss_app_notification(&id, cx);
+        }
+        settings::MigrationStatus::Failed { error: err } => {
+            if !showed_parse_error {
                 show_app_notification(id, cx, move |cx| {
                     cx.new(|cx| {
                         MessageNotification::new(
                             format!(
                                 "Failed to migrate settings\n\
-                                    {err}"
+                                {err}"
                             ),
                             cx,
                         )
@@ -1412,26 +1431,17 @@ pub fn handle_settings_file_changes(
                         })
                     })
                 });
-                // notify user here
-                false
             }
-        };
-
-        if let settings::ParseStatus::Failed { error: err } = &result.parse_status {
-            let settings_type = if is_user { "user" } else { "global" };
-            log::error!("Failed to load {} settings: {err}", settings_type);
         }
-
-        settings_changed(
-            match result.parse_status {
-                settings::ParseStatus::Failed { error } => Some(anyhow::format_err!(error)),
-                settings::ParseStatus::Success => None,
-            },
-            cx,
-        );
-
-        content_migrated
     };
+}
+
+pub fn handle_settings_file_changes(
+    mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
+    mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
+    cx: &mut App,
+) {
+    MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
 
     // Initial load of both settings files
     let global_content = cx
@@ -1444,8 +1454,8 @@ pub fn handle_settings_file_changes(
         .unwrap();
 
     SettingsStore::update_global(cx, |store, cx| {
-        process_settings(global_content, false, store, cx);
-        process_settings(user_content, true, store, cx);
+        notify_settings_errors(store.set_user_settings(&user_content, cx), true, cx);
+        notify_settings_errors(store.set_global_settings(&global_content, cx), false, cx);
     });
 
     // Watch for changes in both files
@@ -1462,7 +1472,14 @@ pub fn handle_settings_file_changes(
             };
 
             let result = cx.update_global(|store: &mut SettingsStore, cx| {
-                let migrating_in_memory = process_settings(content, is_user, store, cx);
+                let result = if is_user {
+                    store.set_user_settings(&content, cx)
+                } else {
+                    store.set_global_settings(&content, cx)
+                };
+                let migrating_in_memory =
+                    matches!(&result.migration_status, MigrationStatus::Succeeded);
+                notify_settings_errors(result, is_user, cx);
                 if let Some(notifier) = MigrationNotification::try_global(cx) {
                     notifier.update(cx, |_, cx| {
                         cx.emit(MigrationEvent::ContentChanged {
@@ -1725,36 +1742,6 @@ pub fn load_default_keymap(cx: &mut App) {
     }
 }
 
-pub fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut App) {
-    struct SettingsParseErrorNotification;
-    let id = NotificationId::unique::<SettingsParseErrorNotification>();
-
-    match error {
-        Some(error) => {
-            if let Some(InvalidSettingsError::LocalSettings { .. }) =
-                error.downcast_ref::<InvalidSettingsError>()
-            {
-                // Local settings errors are displayed by the projects
-                return;
-            }
-            show_app_notification(id, cx, move |cx| {
-                cx.new(|cx| {
-                    MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
-                        .primary_message("Open Settings File")
-                        .primary_icon(IconName::Settings)
-                        .primary_on_click(|window, cx| {
-                            window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx);
-                            cx.emit(DismissEvent);
-                        })
-                })
-            });
-        }
-        None => {
-            dismiss_app_notification(&id, cx);
-        }
-    }
-}
-
 pub fn open_new_ssh_project_from_project(
     workspace: &mut Workspace,
     paths: Vec<PathBuf>,
@@ -4497,7 +4484,7 @@ mod tests {
                 app_state.fs.clone(),
                 PathBuf::from("/global_settings.json"),
             );
-            handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
+            handle_settings_file_changes(settings_rx, global_settings_rx, cx);
             handle_keymap_file_changes(keymap_rx, cx);
         });
         workspace
@@ -4615,7 +4602,7 @@ mod tests {
                 app_state.fs.clone(),
                 PathBuf::from("/global_settings.json"),
             );
-            handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
+            handle_settings_file_changes(settings_rx, global_settings_rx, cx);
             handle_keymap_file_changes(keymap_rx, cx);
         });