diff --git a/helix-lsp/src/jsonrpc.rs b/helix-lsp/src/jsonrpc.rs index 9ff57cde941f..0a5b2b4c34c4 100644 --- a/helix-lsp/src/jsonrpc.rs +++ b/helix-lsp/src/jsonrpc.rs @@ -104,10 +104,37 @@ impl std::error::Error for Error {} #[serde(untagged)] pub enum Id { Null, - Num(u64), + Num(#[serde(deserialize_with = "deserialize_jsonrpc_id_num")] u64), Str(String), } +fn deserialize_jsonrpc_id_num<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let num = serde_json::Number::deserialize(deserializer)?; + + if let Some(val) = num.as_u64() { + return Ok(val); + }; + + // Accept floats as long as they represent positive whole numbers. + // The JSONRPC spec says "Numbers SHOULD NOT contain fractional parts" so we should try to + // accept them if possible. The JavaScript type system lumps integers and floats together so + // some languages may serialize integer IDs as floats with a zeroed fractional part. + // See . + if let Some(val) = num + .as_f64() + .filter(|f| f.is_sign_positive() && f.fract() == 0.0) + { + return Ok(val as u64); + } + + Err(de::Error::custom( + "number must be integer or float representing a whole number in valid u64 range", + )) +} + impl std::fmt::Display for Id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -375,6 +402,22 @@ fn serialize_skip_none_params() { assert_eq!(serialized, r#"{"jsonrpc":"2.0","method":"exit"}"#); } +#[test] +fn id_deserialize() { + use serde_json; + + let id = r#"8"#; + let deserialized: Id = serde_json::from_str(id).unwrap(); + assert_eq!(deserialized, Id::Num(8)); + + let id = r#"4.0"#; + let deserialized: Id = serde_json::from_str(id).unwrap(); + assert_eq!(deserialized, Id::Num(4)); + + let id = r#"0.01"#; + assert!(serde_json::from_str::(id).is_err()); +} + #[test] fn success_output_deserialize() { use serde_json;