settings_ui: Fix IEEE 754 floating point error when serializing value to JSON (#40677)

Closes #40556

Release Notes:

- settings-ui: Fixed an issue where modifying floating point number fields could result in the values written to the settings file containing IEEE 754 floating point error

> [!Note]
> Seems like there's another pull request fixing the centered layout in
#40661 by normalizing the whole `settings.json` file when updating it.
Not sure which is the better way to handle this issue.

## Description

This pull request solves the IEEE 754 floating point error when
serializing values from settings-ui into `settings.json` by creating a
two `serde_helper` to convert the problematic f32 into a formatted two
decimal placed f32.

Fields currently exists the IEEE 754 error:
- Appearance → Unnecessary Code Fade
- Editor → Drop Size Target
- Window & Layout → Centered Layout Left/Right Padding
- Window & Layout → Inactive Opacity

## How to verify

### Unnecessary Code Fade

As Is | To Be
--- | ---
<video
src="https://github.com/user-attachments/assets/1bf4bad2-63c5-4b03-ac29-8b6b59569e16"
/> | <video
src="https://github.com/user-attachments/assets/dadcd4a1-651b-43dd-913f-edae073ceb68"
/>

### Drop Size Target

As Is | To Be
--- | ---
<video
src="https://github.com/user-attachments/assets/9d5b4173-fcac-44d0-b7fc-772a2e426ef1"
/> | <video
src="https://github.com/user-attachments/assets/4b5adeaf-e678-494d-bd1b-6c1d55824c43"
/>

### Centered Layout Left/Right Padding

As Is | To Be
--- | ---
<video
src="https://github.com/user-attachments/assets/33b4e1ff-7ab2-44f7-9e9b-8abad1565d9a"
/> | <video
src="https://github.com/user-attachments/assets/63d8de9e-28d1-4bd7-a6c9-02452e105486"
/>

### Inactive Opacity

As Is | To Be
--- | ---
<video
src="https://github.com/user-attachments/assets/a7fe2e72-deb6-41dc-82f3-e2649503b8a4"
/> | <video
src="https://github.com/user-attachments/assets/993c314f-b6f6-4dcd-8f74-fa357ab063e9"
/>

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
Dong
2025-10-21 16:45:40 +00:00
committed by GitHub
co-authored by Ben Kunkle
parent fa550de922
commit 641ae90cd6
9 changed files with 181 additions and 5 deletions
+135
View File
@@ -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<S>(
value: &f32,
serializer: S,
) -> Result<S::Ok, S::Error>
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<f32>` 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<f32>,
/// }
/// ```
pub fn serialize_optional_f32_with_two_decimal_places<S>(
value: &Option<f32>,
serializer: S,
) -> Result<S::Ok, S::Error>
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<f32>,
}
#[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);
}
}
}
+2
View File
@@ -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::{
+5
View File
@@ -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<f32>,
/// 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<f32>,
}
@@ -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<f32>,
}
@@ -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<f32>,
/// 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<f32>,
/// Whether to reveal it in the outline panel automatically,
/// when a corresponding project entry becomes active.
@@ -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<f32>,
/// 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<f32>,
/// The default model to use when creating new chats and for other features when a specific model is not specified.
pub default_model: Option<LanguageModelSelection>,
@@ -232,6 +234,7 @@ pub enum CompletionMode {
pub struct LanguageModelParameters {
pub provider: Option<LanguageModelProviderSetting>,
pub model: Option<SharedString>,
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
pub temperature: Option<f32>,
}
+13 -3
View File
@@ -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<f32>,
/// 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<f32>,
/// 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<f32>,
/// 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<f32>,
/// 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<f32> 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 {
@@ -46,6 +46,7 @@ pub struct AnthropicAvailableModel {
/// Configuration of Anthropic's caching API.
pub cache_configuration: Option<LanguageModelCacheConfiguration>,
pub max_output_tokens: Option<u64>,
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
pub default_temperature: Option<f32>,
#[serde(default)]
pub extra_beta_headers: Vec<String>,
@@ -71,6 +72,7 @@ pub struct BedrockAvailableModel {
pub max_tokens: u64,
pub cache_configuration: Option<LanguageModelCacheConfiguration>,
pub max_output_tokens: Option<u64>,
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
pub default_temperature: Option<f32>,
pub mode: Option<ModelMode>,
}
@@ -332,6 +334,7 @@ pub struct ZedDotDevAvailableModel {
/// Indicates whether this custom model supports caching.
pub cache_configuration: Option<LanguageModelCacheConfiguration>,
/// The default temperature to use for this model.
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
pub default_temperature: Option<f32>,
/// Any extra beta headers to provide when using the model.
#[serde(default)]
@@ -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<f32>,
/// Sets the terminal's font family.
///
@@ -61,6 +62,7 @@ pub struct TerminalSettingsContent {
pub line_height: Option<TerminalLineHeight>,
pub font_features: Option<FontFeatures>,
/// 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<f32>,
/// 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<f32>,
/// 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<f32>,
/// 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<f32>,
}
@@ -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 {
@@ -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<f32>,
/// 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<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)]
@@ -56,9 +60,11 @@ pub struct ThemeSettingsContent {
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)]
@@ -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 {
@@ -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<f32>,
/// 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<f32>,
/// 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<f32>,
// 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<f32>,
}
@@ -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<f32>,
/// 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<f32>,
/// Whether to reveal it in the project panel automatically,
/// when a corresponding project entry becomes active.