settings: accept trailing commas (#2606)
Z-2357 I've found a crate that handles both comments and trailing commas in JSON. It is a fork of `serde_json`, but it is maintained & up-to-date. Sadly RawValue seems to not play nicely with it; I've ran into deserialisation issues around use of RawValue. For this PR I've migrated to `Value` API. Obviously this is just a point of discussion, not something I'd merge straight away. There may be better solutions to this particular problem. I've also noticed that `serde_json_lenient` does not handle trailing commas after bindings array. I'm not sure how big of an issue that is. Release Notes: - Improved handling of trailing commas in settings files. [#1322](https://github.com/zed-industries/community/issues/1322)
This commit is contained in:
parent
70ccbbafc1
commit
2a3c660d1f
7 changed files with 81 additions and 40 deletions
43
Cargo.lock
generated
43
Cargo.lock
generated
|
@ -593,7 +593,7 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
"itoa",
|
"itoa 1.0.6",
|
||||||
"matchit",
|
"matchit",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mime",
|
"mime",
|
||||||
|
@ -3011,7 +3011,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 1.4.0",
|
"bytes 1.4.0",
|
||||||
"fnv",
|
"fnv",
|
||||||
"itoa",
|
"itoa 1.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3070,7 +3070,7 @@ dependencies = [
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa 1.0.6",
|
||||||
"pin-project-lite 0.2.9",
|
"pin-project-lite 0.2.9",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -3336,6 +3336,12 @@ dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
@ -3396,12 +3402,6 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "json_comments"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "41ee439ee368ba4a77ac70d04f14015415af8600d6c894dc1f11bd79758c57d5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jwt"
|
name = "jwt"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
|
@ -5667,7 +5667,7 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno 0.2.8",
|
"errno 0.2.8",
|
||||||
"io-lifetimes 0.5.3",
|
"io-lifetimes 0.5.3",
|
||||||
"itoa",
|
"itoa 1.0.6",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.0.42",
|
"linux-raw-sys 0.0.42",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
@ -6099,7 +6099,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"itoa",
|
"itoa 1.0.6",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json_lenient"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa 0.4.8",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -6122,7 +6134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"itoa",
|
"itoa 1.0.6",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
@ -6148,7 +6160,7 @@ dependencies = [
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui",
|
"gpui",
|
||||||
"json_comments",
|
"indoc",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"postage",
|
"postage",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
|
@ -6157,6 +6169,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_json_lenient",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlez",
|
"sqlez",
|
||||||
"staff_mode",
|
"staff_mode",
|
||||||
|
@ -6507,7 +6520,7 @@ dependencies = [
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"hmac 0.12.1",
|
"hmac 0.12.1",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"itoa",
|
"itoa 1.0.6",
|
||||||
"libc",
|
"libc",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
"log",
|
"log",
|
||||||
|
@ -6993,7 +7006,7 @@ version = "0.3.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc"
|
checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa 1.0.6",
|
||||||
"serde",
|
"serde",
|
||||||
"time-core",
|
"time-core",
|
||||||
"time-macros",
|
"time-macros",
|
||||||
|
|
|
@ -445,7 +445,7 @@ type WindowBoundsCallback = Box<dyn FnMut(WindowBounds, Uuid, &mut WindowContext
|
||||||
type KeystrokeCallback =
|
type KeystrokeCallback =
|
||||||
Box<dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut WindowContext) -> bool>;
|
Box<dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut WindowContext) -> bool>;
|
||||||
type ActiveLabeledTasksCallback = Box<dyn FnMut(&mut AppContext) -> bool>;
|
type ActiveLabeledTasksCallback = Box<dyn FnMut(&mut AppContext) -> bool>;
|
||||||
type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
|
type DeserializeActionCallback = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
|
||||||
type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut AppContext) -> bool>;
|
type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut AppContext) -> bool>;
|
||||||
|
|
||||||
pub struct AppContext {
|
pub struct AppContext {
|
||||||
|
@ -624,14 +624,14 @@ impl AppContext {
|
||||||
pub fn deserialize_action(
|
pub fn deserialize_action(
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
argument: Option<&str>,
|
argument: Option<serde_json::Value>,
|
||||||
) -> Result<Box<dyn Action>> {
|
) -> Result<Box<dyn Action>> {
|
||||||
let callback = self
|
let callback = self
|
||||||
.action_deserializers
|
.action_deserializers
|
||||||
.get(name)
|
.get(name)
|
||||||
.ok_or_else(|| anyhow!("unknown action {}", name))?
|
.ok_or_else(|| anyhow!("unknown action {}", name))?
|
||||||
.1;
|
.1;
|
||||||
callback(argument.unwrap_or("{}"))
|
callback(argument.unwrap_or_else(|| serde_json::Value::Object(Default::default())))
|
||||||
.with_context(|| format!("invalid data for action {}", name))
|
.with_context(|| format!("invalid data for action {}", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5573,7 +5573,7 @@ mod tests {
|
||||||
let action1 = cx
|
let action1 = cx
|
||||||
.deserialize_action(
|
.deserialize_action(
|
||||||
"test::something::ComplexAction",
|
"test::something::ComplexAction",
|
||||||
Some(r#"{"arg": "a", "count": 5}"#),
|
Some(serde_json::from_str(r#"{"arg": "a", "count": 5}"#).unwrap()),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let action2 = cx
|
let action2 = cx
|
||||||
|
|
|
@ -11,7 +11,7 @@ pub trait Action: 'static {
|
||||||
fn qualified_name() -> &'static str
|
fn qualified_name() -> &'static str
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
fn from_json_str(json: &str) -> anyhow::Result<Box<dyn Action>>
|
fn from_json_str(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ macro_rules! actions {
|
||||||
$crate::__impl_action! {
|
$crate::__impl_action! {
|
||||||
$namespace,
|
$namespace,
|
||||||
$name,
|
$name,
|
||||||
fn from_json_str(_: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
|
fn from_json_str(_: $crate::serde_json::Value) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
|
||||||
Ok(Box::new(Self))
|
Ok(Box::new(Self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,8 +58,8 @@ macro_rules! impl_actions {
|
||||||
$crate::__impl_action! {
|
$crate::__impl_action! {
|
||||||
$namespace,
|
$namespace,
|
||||||
$name,
|
$name,
|
||||||
fn from_json_str(json: &str) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
|
fn from_json_str(json: $crate::serde_json::Value) -> $crate::anyhow::Result<Box<dyn $crate::Action>> {
|
||||||
Ok(Box::new($crate::serde_json::from_str::<Self>(json)?))
|
Ok(Box::new($crate::serde_json::from_value::<Self>(json)?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
|
|
@ -394,7 +394,7 @@ impl<'a> WindowContext<'a> {
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(move |(name, (type_id, deserialize))| {
|
.filter_map(move |(name, (type_id, deserialize))| {
|
||||||
if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() {
|
if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() {
|
||||||
let action = deserialize("{}").ok()?;
|
let action = deserialize(serde_json::Value::Object(Default::default())).ok()?;
|
||||||
let bindings = self
|
let bindings = self
|
||||||
.keystroke_matcher
|
.keystroke_matcher
|
||||||
.bindings_for_action_type(*type_id)
|
.bindings_for_action_type(*type_id)
|
||||||
|
|
|
@ -21,7 +21,7 @@ util = { path = "../util" }
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
json_comments = "0.2"
|
serde_json_lenient = {version = "0.1", features = ["preserve_order", "raw_value"]}
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
|
@ -37,6 +37,6 @@ tree-sitter-json = "*"
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
fs = { path = "../fs", features = ["test-support"] }
|
fs = { path = "../fs", features = ["test-support"] }
|
||||||
|
indoc.workspace = true
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = "1.3.0"
|
||||||
unindent.workspace = true
|
unindent.workspace = true
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
|
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use gpui::{keymap_matcher::Binding, AppContext};
|
use gpui::{keymap_matcher::Binding, AppContext};
|
||||||
use schemars::{
|
use schemars::{
|
||||||
|
@ -8,7 +8,7 @@ use schemars::{
|
||||||
JsonSchema,
|
JsonSchema,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{value::RawValue, Value};
|
use serde_json::Value;
|
||||||
use util::{asset_str, ResultExt};
|
use util::{asset_str, ResultExt};
|
||||||
|
|
||||||
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
#[derive(Deserialize, Default, Clone, JsonSchema)]
|
||||||
|
@ -24,7 +24,7 @@ pub struct KeymapBlock {
|
||||||
|
|
||||||
#[derive(Deserialize, Default, Clone)]
|
#[derive(Deserialize, Default, Clone)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct KeymapAction(Box<RawValue>);
|
pub struct KeymapAction(Value);
|
||||||
|
|
||||||
impl JsonSchema for KeymapAction {
|
impl JsonSchema for KeymapAction {
|
||||||
fn schema_name() -> String {
|
fn schema_name() -> String {
|
||||||
|
@ -37,11 +37,12 @@ impl JsonSchema for KeymapAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct ActionWithData(Box<str>, Box<RawValue>);
|
struct ActionWithData(Box<str>, Value);
|
||||||
|
|
||||||
impl KeymapFile {
|
impl KeymapFile {
|
||||||
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
|
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
|
||||||
let content = asset_str::<SettingsAssets>(asset_path);
|
let content = asset_str::<SettingsAssets>(asset_path);
|
||||||
|
|
||||||
Self::parse(content.as_ref())?.add_to_cx(cx)
|
Self::parse(content.as_ref())?.add_to_cx(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,18 +55,27 @@ impl KeymapFile {
|
||||||
let bindings = bindings
|
let bindings = bindings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(keystroke, action)| {
|
.filter_map(|(keystroke, action)| {
|
||||||
let action = action.0.get();
|
let action = action.0;
|
||||||
|
|
||||||
// This is a workaround for a limitation in serde: serde-rs/json#497
|
// This is a workaround for a limitation in serde: serde-rs/json#497
|
||||||
// We want to deserialize the action data as a `RawValue` so that we can
|
// We want to deserialize the action data as a `RawValue` so that we can
|
||||||
// deserialize the action itself dynamically directly from the JSON
|
// deserialize the action itself dynamically directly from the JSON
|
||||||
// string. But `RawValue` currently does not work inside of an untagged enum.
|
// string. But `RawValue` currently does not work inside of an untagged enum.
|
||||||
if action.starts_with('[') {
|
if let Value::Array(items) = action {
|
||||||
let ActionWithData(name, data) = serde_json::from_str(action).log_err()?;
|
let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
|
||||||
cx.deserialize_action(&name, Some(data.get()))
|
return Some(Err(anyhow!("Expected array of length 2")));
|
||||||
|
};
|
||||||
|
let serde_json::Value::String(name) = name else {
|
||||||
|
return Some(Err(anyhow!("Expected first item in array to be a string.")))
|
||||||
|
};
|
||||||
|
cx.deserialize_action(
|
||||||
|
&name,
|
||||||
|
Some(data),
|
||||||
|
)
|
||||||
|
} else if let Value::String(name) = action {
|
||||||
|
cx.deserialize_action(&name, None)
|
||||||
} else {
|
} else {
|
||||||
let name = serde_json::from_str(action).log_err()?;
|
return Some(Err(anyhow!("Expected two-element array, got {:?}", action)));
|
||||||
cx.deserialize_action(name, None)
|
|
||||||
}
|
}
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
|
@ -118,3 +128,24 @@ impl KeymapFile {
|
||||||
serde_json::to_value(root_schema).unwrap()
|
serde_json::to_value(root_schema).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::KeymapFile;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_deserialize_keymap_with_trailing_comma() {
|
||||||
|
let json = indoc::indoc! {"[
|
||||||
|
// Standard macOS bindings
|
||||||
|
{
|
||||||
|
\"bindings\": {
|
||||||
|
\"up\": \"menu::SelectPrev\",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
"
|
||||||
|
|
||||||
|
};
|
||||||
|
KeymapFile::parse(json).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -834,11 +834,8 @@ fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len:
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
|
pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
|
||||||
Ok(serde_json::from_reader(
|
Ok(serde_json_lenient::from_str(content)?)
|
||||||
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
|
|
||||||
)?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue