diff --git a/crates/settings/src/serde_helper.rs b/crates/settings/src/serde_helper.rs new file mode 100644 index 0000000000000000000000000000000000000000..1c1826abd4a66dcfdb51652a331218b769aa0881 --- /dev/null +++ b/crates/settings/src/serde_helper.rs @@ -0,0 +1,135 @@ +use serde::Serializer; + +/// Serializes an f32 value with 2 decimal places of precision. +/// +/// This function rounds the value to 2 decimal places and formats it as a string, +/// then parses it back to f64 before serialization. This ensures clean JSON output +/// without IEEE 754 floating-point artifacts. +/// +/// # Arguments +/// +/// * `value` - The f32 value to serialize +/// * `serializer` - The serde serializer to use +/// +/// # Returns +/// +/// Result of the serialization operation +/// +/// # Usage +/// +/// This function can be used with Serde's `serialize_with` attribute: +/// ``` +/// use serde::Serialize; +/// use settings::serialize_f32_with_two_decimal_places; +/// +/// #[derive(Serialize)] +/// struct ExampleStruct(#[serde(serialize_with = "serialize_f32_with_two_decimal_places")] f32); +/// ``` +pub fn serialize_f32_with_two_decimal_places( + value: &f32, + serializer: S, +) -> Result +where + S: Serializer, +{ + let rounded = (value * 100.0).round() / 100.0; + let formatted = format!("{:.2}", rounded); + let clean_value: f64 = formatted.parse().unwrap_or(rounded as f64); + serializer.serialize_f64(clean_value) +} + +/// Serializes an optional f32 value with 2 decimal places of precision. +/// +/// This function handles `Option` types, serializing `Some` values with 2 decimal +/// places of precision and `None` values as null. For `Some` values, it rounds to 2 decimal +/// places and formats as a string, then parses back to f64 before serialization. This ensures +/// clean JSON output without IEEE 754 floating-point artifacts. +/// +/// # Arguments +/// +/// * `value` - The optional f32 value to serialize +/// * `serializer` - The serde serializer to use +/// +/// # Returns +/// +/// Result of the serialization operation +/// +/// # Behavior +/// +/// * `Some(v)` - Serializes the value rounded to 2 decimal places +/// * `None` - Serializes as JSON null +/// +/// # Usage +/// +/// This function can be used with Serde's `serialize_with` attribute: +/// ``` +/// use serde::Serialize; +/// use settings::serialize_optional_f32_with_two_decimal_places; +/// +/// #[derive(Serialize)] +/// struct ExampleStruct { +/// #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] +/// optional_value: Option, +/// } +/// ``` +pub fn serialize_optional_f32_with_two_decimal_places( + value: &Option, + serializer: S, +) -> Result +where + S: Serializer, +{ + match value { + Some(v) => { + let rounded = (v * 100.0).round() / 100.0; + let formatted = format!("{:.2}", rounded); + let clean_value: f64 = formatted.parse().unwrap_or(rounded as f64); + serializer.serialize_some(&clean_value) + } + None => serializer.serialize_none(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize)] + struct TestOptional { + #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] + value: Option, + } + + #[derive(Serialize, Deserialize)] + struct TestNonOptional { + #[serde(serialize_with = "serialize_f32_with_two_decimal_places")] + value: f32, + } + + #[test] + fn test_serialize_optional_f32_with_two_decimal_places() { + let cases = [ + (Some(123.456789), r#"{"value":123.46}"#), + (Some(1.2), r#"{"value":1.2}"#), + (Some(300.00000), r#"{"value":300.0}"#), + ]; + for (value, expected) in cases { + let value = TestOptional { value }; + assert_eq!(serde_json::to_string(&value).unwrap(), expected); + } + } + + #[test] + fn test_serialize_f32_with_two_decimal_places() { + let cases = [ + (123.456789, r#"{"value":123.46}"#), + (1.200, r#"{"value":1.2}"#), + (300.00000, r#"{"value":300.0}"#), + ]; + for (value, expected) in cases { + let value = TestNonOptional { value }; + assert_eq!(serde_json::to_string(&value).unwrap(), expected); + } + } +} diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 6fe078301abab974ba202660319966e7df42027a..5dad953b32afcd027c0b6c4ec4be36f9659ce022 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -2,6 +2,7 @@ mod base_keymap_setting; mod editable_setting_control; mod keymap_file; pub mod merge_from; +mod serde_helper; mod settings_content; mod settings_file; mod settings_json; @@ -21,6 +22,7 @@ pub use keymap_file::{ KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation, KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult, }; +pub use serde_helper::*; pub use settings_file::*; pub use settings_json::*; pub use settings_store::{ diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 9eec9ac3d56b2ac2fa7d435d658de3f1c2123a1a..1c5b392c4b352da75d8818daaa83a08ee4f2147d 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -515,6 +515,7 @@ pub struct GitPanelSettingsContent { /// Default width of the panel in pixels. /// /// Default: 360 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_width: Option, /// How entry statuses are displayed. /// @@ -586,6 +587,7 @@ pub struct NotificationPanelSettingsContent { /// Default width of the panel in pixels. /// /// Default: 300 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_width: Option, } @@ -603,6 +605,7 @@ pub struct PanelSettingsContent { /// Default width of the panel in pixels. /// /// Default: 240 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_width: Option, } @@ -777,6 +780,7 @@ pub struct OutlinePanelSettingsContent { /// Customize default width (in pixels) taken by outline panel /// /// Default: 240 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_width: Option, /// The position of outline panel /// @@ -797,6 +801,7 @@ pub struct OutlinePanelSettingsContent { /// Amount of indentation (in pixels) for nested items. /// /// Default: 20 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub indent_size: Option, /// Whether to reveal it in the outline panel automatically, /// when a corresponding project entry becomes active. diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index 9d1cd6fed88deb89848a24c890237805b5a8128c..c641f280e177669a2af14e91c844f2a5f059b648 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -26,10 +26,12 @@ pub struct AgentSettingsContent { /// Default width in pixels when the agent panel is docked to the left or right. /// /// Default: 640 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_width: Option, /// Default height in pixels when the agent panel is docked to the bottom. /// /// Default: 320 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_height: Option, /// The default model to use when creating new chats and for other features when a specific model is not specified. pub default_model: Option, @@ -232,6 +234,7 @@ pub enum CompletionMode { pub struct LanguageModelParameters { pub provider: Option, pub model: Option, + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub temperature: Option, } diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs index 4b00cd24500999eb917bf2117fd17b557d2509ae..f5ec805c72d054744477c73cefa50b9bc3fcc7d1 100644 --- a/crates/settings/src/settings_content/editor.rs +++ b/crates/settings/src/settings_content/editor.rs @@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use settings_macros::MergeFrom; -use crate::{DelayMs, DiagnosticSeverityContent, ShowScrollbar}; +use crate::{ + DelayMs, DiagnosticSeverityContent, ShowScrollbar, serialize_f32_with_two_decimal_places, +}; #[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] @@ -70,6 +72,7 @@ pub struct EditorSettingsContent { /// The number of lines to keep above/below the cursor when auto-scrolling. /// /// Default: 3. + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub vertical_scroll_margin: Option, /// Whether to scroll when clicking near the edge of the visible text area. /// @@ -78,17 +81,20 @@ pub struct EditorSettingsContent { /// The number of characters to keep on either side when scrolling with the mouse. /// /// Default: 5. + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub horizontal_scroll_margin: Option, /// Scroll sensitivity multiplier. This multiplier is applied /// to both the horizontal and vertical delta values while scrolling. /// /// Default: 1.0 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub scroll_sensitivity: Option, /// Scroll sensitivity multiplier for fast scrolling. This multiplier is applied /// to both the horizontal and vertical delta values while scrolling. Fast scrolling /// happens when a user holds the alt or option key while scrolling. /// /// Default: 4.0 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub fast_scroll_sensitivity: Option, /// Whether the line numbers on editors gutter are relative or not. /// @@ -796,7 +802,9 @@ pub enum DisplayIn { derive_more::FromStr, )] #[serde(transparent)] -pub struct MinimumContrast(pub f32); +pub struct MinimumContrast( + #[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] pub f32, +); impl Display for MinimumContrast { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -827,7 +835,9 @@ impl From for MinimumContrast { derive_more::FromStr, )] #[serde(transparent)] -pub struct InactiveOpacity(pub f32); +pub struct InactiveOpacity( + #[serde(serialize_with = "serialize_f32_with_two_decimal_places")] pub f32, +); impl Display for InactiveOpacity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/crates/settings/src/settings_content/language_model.rs b/crates/settings/src/settings_content/language_model.rs index 7139aac5eaca5c521007b21dbbb665bb4855347a..a0aa57a970c2483e4d9c617506d7b869c223cdf0 100644 --- a/crates/settings/src/settings_content/language_model.rs +++ b/crates/settings/src/settings_content/language_model.rs @@ -46,6 +46,7 @@ pub struct AnthropicAvailableModel { /// Configuration of Anthropic's caching API. pub cache_configuration: Option, pub max_output_tokens: Option, + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_temperature: Option, #[serde(default)] pub extra_beta_headers: Vec, @@ -71,6 +72,7 @@ pub struct BedrockAvailableModel { pub max_tokens: u64, pub cache_configuration: Option, pub max_output_tokens: Option, + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_temperature: Option, pub mode: Option, } @@ -332,6 +334,7 @@ pub struct ZedDotDevAvailableModel { /// Indicates whether this custom model supports caching. pub cache_configuration: Option, /// The default temperature to use for this model. + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_temperature: Option, /// Any extra beta headers to provide when using the model. #[serde(default)] diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs index e1b101e5ae72a260ec90c01a63315b2b1c3f2c11..d6e82b40b439ba2308a3c9be594e5d895e9c1dad 100644 --- a/crates/settings/src/settings_content/terminal.rs +++ b/crates/settings/src/settings_content/terminal.rs @@ -41,6 +41,7 @@ pub struct TerminalSettingsContent { /// /// If this option is not included, /// the terminal will default to matching the buffer's font size. + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub font_size: Option, /// Sets the terminal's font family. /// @@ -61,6 +62,7 @@ pub struct TerminalSettingsContent { pub line_height: Option, pub font_features: Option, /// Sets the terminal's font weight in CSS weight units 0-900. + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub font_weight: Option, /// Default cursor shape for the terminal. /// Can be "bar", "block", "underline", or "hollow". @@ -99,10 +101,12 @@ pub struct TerminalSettingsContent { /// Default width when the terminal is docked to the left or right. /// /// Default: 640 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_width: Option, /// Default height when the terminal is docked to the bottom. /// /// Default: 320 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_height: Option, /// The maximum number of lines to keep in the scrollback history. /// Maximum allowed value is 100_000, all values above that will be treated as 100_000. @@ -130,6 +134,7 @@ pub struct TerminalSettingsContent { /// - 90: Preferred for body text /// /// Default: 45 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub minimum_contrast: Option, } @@ -213,7 +218,7 @@ pub enum TerminalLineHeight { /// particularly if they use box characters Standard, /// Use a custom line height. - Custom(f32), + Custom(#[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] f32), } impl TerminalLineHeight { diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index 487ffad34e313f403d97f91af5df205294df61df..80b543a20aa389ca05b13371f235ebc0dda9c82e 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -9,6 +9,8 @@ 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] @@ -16,6 +18,7 @@ use serde_with::skip_serializing_none; 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, /// The name of a font to use for rendering in the UI. #[serde(default)] @@ -42,6 +45,7 @@ pub struct ThemeSettingsContent { pub buffer_font_fallbacks: Option>, /// 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, /// The weight of the editor font in CSS units from 100 to 900. #[serde(default)] @@ -56,9 +60,11 @@ pub struct ThemeSettingsContent { pub buffer_font_features: Option, /// 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, /// 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, /// The name of the Zed theme to use. #[serde(default)] @@ -104,7 +110,7 @@ pub struct ThemeSettingsContent { derive_more::FromStr, )] #[serde(transparent)] -pub struct CodeFade(pub f32); +pub struct CodeFade(#[serde(serialize_with = "serialize_f32_with_two_decimal_places")] pub f32); impl Display for CodeFade { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index a4e36ef8358487dbd4f3e5696eb52c5da9d28eb9..a76a1347d4431d9479abed6ab7aa5a893f0a438e 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -8,6 +8,7 @@ use settings_macros::MergeFrom; use crate::{ DelayMs, DockPosition, DockSide, InactiveOpacity, ScrollbarSettingsContent, ShowIndentGuides, + serialize_optional_f32_with_two_decimal_places, }; #[skip_serializing_none] @@ -58,6 +59,7 @@ pub struct WorkspaceSettingsContent { /// Given as a fraction that will be multiplied by the smaller dimension of the workspace. /// /// Default: `0.2` (20% of the smaller dimension of the workspace) + #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] pub drop_target_size: Option, /// Whether to close the window when using 'close active item' on a workspace with no tabs /// @@ -251,6 +253,7 @@ pub struct ActivePaneModifiers { /// The border is drawn inset. /// /// Default: `0.0` + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub border_size: Option, /// Opacity of inactive panels. /// When set to 1.0, the inactive panes have the same opacity as the active one. @@ -468,11 +471,13 @@ pub struct CenteredLayoutSettings { /// workspace when the centered layout is used. /// /// Default: 0.2 + #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] pub left_padding: Option, // The relative width of the right padding of the central pane from the // workspace when the centered layout is used. /// /// Default: 0.2 + #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] pub right_padding: Option, } @@ -521,6 +526,7 @@ pub struct ProjectPanelSettingsContent { /// Customize default width (in pixels) taken by project panel /// /// Default: 240 + #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_width: Option, /// The position of project panel /// @@ -545,6 +551,7 @@ pub struct ProjectPanelSettingsContent { /// Amount of indentation (in pixels) for nested items. /// /// Default: 20 + #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] pub indent_size: Option, /// Whether to reveal it in the project panel automatically, /// when a corresponding project entry becomes active.