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:
Piotr Osiewicz 2023-06-19 18:29:03 +02:00 committed by GitHub
parent 70ccbbafc1
commit 2a3c660d1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 81 additions and 40 deletions

View file

@ -1,5 +1,5 @@
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use collections::BTreeMap;
use gpui::{keymap_matcher::Binding, AppContext};
use schemars::{
@ -8,7 +8,7 @@ use schemars::{
JsonSchema,
};
use serde::Deserialize;
use serde_json::{value::RawValue, Value};
use serde_json::Value;
use util::{asset_str, ResultExt};
#[derive(Deserialize, Default, Clone, JsonSchema)]
@ -24,7 +24,7 @@ pub struct KeymapBlock {
#[derive(Deserialize, Default, Clone)]
#[serde(transparent)]
pub struct KeymapAction(Box<RawValue>);
pub struct KeymapAction(Value);
impl JsonSchema for KeymapAction {
fn schema_name() -> String {
@ -37,11 +37,12 @@ impl JsonSchema for KeymapAction {
}
#[derive(Deserialize)]
struct ActionWithData(Box<str>, Box<RawValue>);
struct ActionWithData(Box<str>, Value);
impl KeymapFile {
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
let content = asset_str::<SettingsAssets>(asset_path);
Self::parse(content.as_ref())?.add_to_cx(cx)
}
@ -54,18 +55,27 @@ impl KeymapFile {
let bindings = bindings
.into_iter()
.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
// We want to deserialize the action data as a `RawValue` so that we can
// deserialize the action itself dynamically directly from the JSON
// string. But `RawValue` currently does not work inside of an untagged enum.
if action.starts_with('[') {
let ActionWithData(name, data) = serde_json::from_str(action).log_err()?;
cx.deserialize_action(&name, Some(data.get()))
if let Value::Array(items) = action {
let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
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 {
let name = serde_json::from_str(action).log_err()?;
cx.deserialize_action(name, None)
return Some(Err(anyhow!("Expected two-element array, got {:?}", action)));
}
.with_context(|| {
format!(
@ -118,3 +128,24 @@ impl KeymapFile {
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();
}
}

View file

@ -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> {
Ok(serde_json::from_reader(
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
)?)
Ok(serde_json_lenient::from_str(content)?)
}
#[cfg(test)]
mod tests {
use super::*;