mod agent; mod editor; mod extension; mod language; mod language_model; mod project; mod terminal; mod theme; mod workspace; pub use agent::*; pub use editor::*; pub use extension::*; pub use language::*; pub use language_model::*; pub use project::*; pub use terminal::*; pub use theme::*; pub use workspace::*; use collections::{HashMap, IndexMap}; use gpui::{App, SharedString}; use release_channel::ReleaseChannel; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use settings_macros::MergeFrom; use std::collections::BTreeSet; use std::env; use std::sync::Arc; pub use util::serde::default_true; use crate::{ActiveSettingsProfileName, merge_from}; #[skip_serializing_none] #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] pub struct SettingsContent { #[serde(flatten)] pub project: ProjectSettingsContent, #[serde(flatten)] pub theme: Box, #[serde(flatten)] pub extension: ExtensionSettingsContent, #[serde(flatten)] pub workspace: WorkspaceSettingsContent, #[serde(flatten)] pub editor: EditorSettingsContent, #[serde(flatten)] pub remote: RemoteSettingsContent, /// Settings related to the file finder. pub file_finder: Option, pub git_panel: Option, pub tabs: Option, pub tab_bar: Option, pub status_bar: Option, pub preview_tabs: Option, pub agent: Option, pub agent_servers: Option, /// Configuration of audio in Zed. pub audio: Option, /// Whether or not to automatically check for updates. /// /// Default: true pub auto_update: Option, /// This base keymap settings adjusts the default keybindings in Zed to be similar /// to other common code editors. By default, Zed's keymap closely follows VSCode's /// keymap, with minor adjustments, this corresponds to the "VSCode" setting. /// /// Default: VSCode pub base_keymap: Option, /// Configuration for the collab panel visual settings. pub collaboration_panel: Option, pub debugger: Option, /// Configuration for Diagnostics-related features. pub diagnostics: Option, /// Configuration for Git-related features pub git: Option, /// Common language server settings. pub global_lsp_settings: Option, /// The settings for the image viewer. pub image_viewer: Option, pub repl: Option, /// Whether or not to enable Helix mode. /// /// Default: false pub helix_mode: Option, pub journal: Option, /// A map of log scopes to the desired log level. /// Useful for filtering out noisy logs or enabling more verbose logging. /// /// Example: {"log": {"client": "warn"}} pub log: Option>, pub line_indicator_format: Option, pub language_models: Option, pub outline_panel: Option, pub project_panel: Option, /// Configuration for the Message Editor pub message_editor: Option, /// Configuration for Node-related features pub node: Option, /// Configuration for the Notification Panel pub notification_panel: Option, pub proxy: Option, /// The URL of the Zed server to connect to. pub server_url: Option, /// Configuration for session-related features pub session: Option, /// Control what info is collected by Zed. pub telemetry: Option, /// Configuration of the terminal in Zed. pub terminal: Option, pub title_bar: Option, /// Whether or not to enable Vim mode. /// /// Default: false pub vim_mode: Option, // Settings related to calls in Zed pub calls: Option, /// Whether to disable all AI features in Zed. /// /// Default: false pub disable_ai: Option, /// Settings related to Vim mode in Zed. pub vim: Option, } impl SettingsContent { pub fn languages_mut(&mut self) -> &mut HashMap { &mut self.project.all_languages.languages.0 } } #[skip_serializing_none] #[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] pub struct UserSettingsContent { #[serde(flatten)] pub content: Box, pub dev: Option>, pub nightly: Option>, pub preview: Option>, pub stable: Option>, pub macos: Option>, pub windows: Option>, pub linux: Option>, #[serde(default)] pub profiles: IndexMap, } pub struct ExtensionsSettingsContent { pub all_languages: AllLanguageSettingsContent, } impl UserSettingsContent { pub fn for_release_channel(&self) -> Option<&SettingsContent> { match *release_channel::RELEASE_CHANNEL { ReleaseChannel::Dev => self.dev.as_deref(), ReleaseChannel::Nightly => self.nightly.as_deref(), ReleaseChannel::Preview => self.preview.as_deref(), ReleaseChannel::Stable => self.stable.as_deref(), } } pub fn for_os(&self) -> Option<&SettingsContent> { match env::consts::OS { "macos" => self.macos.as_deref(), "linux" => self.linux.as_deref(), "windows" => self.windows.as_deref(), _ => None, } } pub fn for_profile(&self, cx: &App) -> Option<&SettingsContent> { let Some(active_profile) = cx.try_global::() else { return None; }; self.profiles.get(&active_profile.0) } } /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// /// Default: VSCode #[derive( Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default, strum::VariantArray, )] pub enum BaseKeymapContent { #[default] VSCode, JetBrains, SublimeText, Atom, TextMate, Emacs, Cursor, None, } impl strum::VariantNames for BaseKeymapContent { const VARIANTS: &'static [&'static str] = &[ "VSCode", "JetBrains", "Sublime Text", "Atom", "TextMate", "Emacs", "Cursor", "None", ]; } #[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] pub struct TitleBarSettingsContent { /// Whether to show the branch icon beside branch switcher in the title bar. /// /// Default: false pub show_branch_icon: Option, /// Whether to show onboarding banners in the title bar. /// /// Default: true pub show_onboarding_banner: Option, /// Whether to show user avatar in the title bar. /// /// Default: true pub show_user_picture: Option, /// Whether to show the branch name button in the titlebar. /// /// Default: true pub show_branch_name: Option, /// Whether to show the project host and name in the titlebar. /// /// Default: true pub show_project_items: Option, /// Whether to show the sign in button in the title bar. /// /// Default: true pub show_sign_in: Option, /// Whether to show the menus in the title bar. /// /// Default: false pub show_menus: Option, } /// Configuration of audio in Zed. #[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] pub struct AudioSettingsContent { /// Opt into the new audio system. /// /// You need to rejoin a call for this setting to apply #[serde(rename = "experimental.rodio_audio")] pub rodio_audio: Option, // default is false /// Requires 'rodio_audio: true' /// /// Automatically increase or decrease you microphone's volume. This affects how /// loud you sound to others. /// /// Recommended: off (default) /// Microphones are too quite in zed, until everyone is on experimental /// audio and has auto speaker volume on this will make you very loud /// compared to other speakers. #[serde(rename = "experimental.auto_microphone_volume")] pub auto_microphone_volume: Option, /// Requires 'rodio_audio: true' /// /// Automatically increate or decrease the volume of other call members. /// This only affects how things sound for you. #[serde(rename = "experimental.auto_speaker_volume")] pub auto_speaker_volume: Option, /// Requires 'rodio_audio: true' /// /// Remove background noises. Works great for typing, cars, dogs, AC. Does /// not work well on music. #[serde(rename = "experimental.denoise")] pub denoise: Option, /// Requires 'rodio_audio: true' /// /// Use audio parameters compatible with the previous versions of /// experimental audio and non-experimental audio. When this is false you /// will sound strange to anyone not on the latest experimental audio. In /// the future we will migrate by setting this to false /// /// You need to rejoin a call for this setting to apply #[serde(rename = "experimental.legacy_audio_compatible")] pub legacy_audio_compatible: Option, } /// Control what info is collected by Zed. #[skip_serializing_none] #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug, MergeFrom)] pub struct TelemetrySettingsContent { /// Send debug info like crash reports. /// /// Default: true pub diagnostics: Option, /// Send anonymized usage data like what languages you're using Zed with. /// /// Default: true pub metrics: Option, } impl Default for TelemetrySettingsContent { fn default() -> Self { Self { diagnostics: Some(true), metrics: Some(true), } } } #[skip_serializing_none] #[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone, MergeFrom)] pub struct DebuggerSettingsContent { /// Determines the stepping granularity. /// /// Default: line pub stepping_granularity: Option, /// Whether the breakpoints should be reused across Zed sessions. /// /// Default: true pub save_breakpoints: Option, /// Whether to show the debug button in the status bar. /// /// Default: true pub button: Option, /// Time in milliseconds until timeout error when connecting to a TCP debug adapter /// /// Default: 2000ms pub timeout: Option, /// Whether to log messages between active debug adapters and Zed /// /// Default: true pub log_dap_communications: Option, /// Whether to format dap messages in when adding them to debug adapter logger /// /// Default: true pub format_dap_log_messages: Option, /// The dock position of the debug panel /// /// Default: Bottom pub dock: Option, } /// The granularity of one 'step' in the stepping requests `next`, `stepIn`, `stepOut`, and `stepBack`. #[derive( PartialEq, Eq, Debug, Hash, Clone, Copy, Deserialize, Serialize, JsonSchema, MergeFrom, strum::VariantArray, strum::VariantNames, )] #[serde(rename_all = "snake_case")] pub enum SteppingGranularity { /// The step should allow the program to run until the current statement has finished executing. /// The meaning of a statement is determined by the adapter and it may be considered equivalent to a line. /// For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'. Statement, /// The step should allow the program to run until the current source line has executed. Line, /// The step should allow one instruction to execute (e.g. one x86 instruction). Instruction, } #[derive( Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, strum::VariantArray, strum::VariantNames, )] #[serde(rename_all = "snake_case")] pub enum DockPosition { Left, Bottom, Right, } /// Settings for slash commands. #[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)] pub struct SlashCommandSettings { /// Settings for the `/cargo-workspace` slash command. pub cargo_workspace: Option, } /// Settings for the `/cargo-workspace` slash command. #[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)] pub struct CargoWorkspaceCommandSettings { /// Whether `/cargo-workspace` is enabled. pub enabled: Option, } /// Configuration of voice calls in Zed. #[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] pub struct CallSettingsContent { /// Whether the microphone should be muted when joining a channel or a call. /// /// Default: false pub mute_on_join: Option, /// Whether your current project should be shared when joining an empty channel. /// /// Default: false pub share_on_join: Option, } #[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] pub struct GitPanelSettingsContent { /// Whether to show the panel button in the status bar. /// /// Default: true pub button: Option, /// Where to dock the panel. /// /// Default: left pub dock: Option, /// 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. /// /// Default: icon pub status_style: Option, /// How and when the scrollbar should be displayed. /// /// Default: inherits editor scrollbar settings pub scrollbar: Option, /// What the default branch name should be when /// `init.defaultBranch` is not set in git /// /// Default: main pub fallback_branch_name: Option, /// Whether to sort entries in the panel by path /// or by status (the default). /// /// Default: false pub sort_by_path: Option, /// Whether to collapse untracked files in the diff panel. /// /// Default: false pub collapse_untracked_diff: Option, } #[derive( Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, strum::VariantArray, strum::VariantNames, )] #[serde(rename_all = "snake_case")] pub enum StatusStyle { #[default] Icon, LabelColor, } #[skip_serializing_none] #[derive( Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, )] pub struct ScrollbarSettings { pub show: Option, } #[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] pub struct NotificationPanelSettingsContent { /// Whether to show the panel button in the status bar. /// /// Default: true pub button: Option, /// Where to dock the panel. /// /// Default: right pub dock: Option, /// Default width of the panel in pixels. /// /// Default: 300 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_width: Option, } #[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] pub struct PanelSettingsContent { /// Whether to show the panel button in the status bar. /// /// Default: true pub button: Option, /// Where to dock the panel. /// /// Default: left pub dock: Option, /// Default width of the panel in pixels. /// /// Default: 240 #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] pub default_width: Option, } #[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] pub struct MessageEditorSettings { /// Whether to automatically replace emoji shortcodes with emoji characters. /// For example: typing `:wave:` gets replaced with `👋`. /// /// Default: false pub auto_replace_emoji_shortcode: Option, } #[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] pub struct FileFinderSettingsContent { /// Whether to show file icons in the file finder. /// /// Default: true pub file_icons: Option, /// Determines how much space the file finder can take up in relation to the available window width. /// /// Default: small pub modal_max_width: Option, /// Determines whether the file finder should skip focus for the active file in search results. /// /// Default: true pub skip_focus_for_active_in_search: Option, /// Determines whether to show the git status in the file finder /// /// Default: true pub git_status: Option, /// Whether to use gitignored files when searching. /// Only the file Zed had indexed will be used, not necessary all the gitignored files. /// /// Default: Smart pub include_ignored: Option, } #[derive( Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, MergeFrom, strum::VariantArray, strum::VariantNames, )] #[serde(rename_all = "snake_case")] pub enum IncludeIgnoredContent { /// Use all gitignored files All, /// Use only the files Zed had indexed Indexed, /// Be smart and search for ignored when called from a gitignored worktree #[default] Smart, } #[derive( Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, MergeFrom, strum::VariantArray, strum::VariantNames, )] #[serde(rename_all = "lowercase")] pub enum FileFinderWidthContent { #[default] Small, Medium, Large, XLarge, Full, } #[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug, JsonSchema, MergeFrom)] pub struct VimSettingsContent { pub default_mode: Option, pub toggle_relative_line_numbers: Option, pub use_system_clipboard: Option, pub use_smartcase_find: Option, pub custom_digraphs: Option>>, pub highlight_on_yank_duration: Option, pub cursor_shape: Option, } #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Debug)] #[serde(rename_all = "snake_case")] pub enum ModeContent { #[default] Normal, Insert, } /// Controls when to use system clipboard. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] #[serde(rename_all = "snake_case")] pub enum UseSystemClipboard { /// Don't use system clipboard. Never, /// Use system clipboard. Always, /// Use system clipboard for yank operations. OnYank, } /// The settings for cursor shape. #[skip_serializing_none] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] pub struct CursorShapeSettings { /// Cursor shape for the normal mode. /// /// Default: block pub normal: Option, /// Cursor shape for the replace mode. /// /// Default: underline pub replace: Option, /// Cursor shape for the visual mode. /// /// Default: block pub visual: Option, /// Cursor shape for the insert mode. /// /// The default value follows the primary cursor_shape. pub insert: Option, } /// Settings specific to journaling #[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] pub struct JournalSettingsContent { /// The path of the directory where journal entries are stored. /// /// Default: `~` pub path: Option, /// What format to display the hours in. /// /// Default: hour12 pub hour_format: Option, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] #[serde(rename_all = "snake_case")] pub enum HourFormat { #[default] Hour12, Hour24, } #[skip_serializing_none] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] pub struct OutlinePanelSettingsContent { /// Whether to show the outline panel button in the status bar. /// /// Default: true pub button: Option, /// 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 /// /// Default: left pub dock: Option, /// Whether to show file icons in the outline panel. /// /// Default: true pub file_icons: Option, /// Whether to show folder icons or chevrons for directories in the outline panel. /// /// Default: true pub folder_icons: Option, /// Whether to show the git status in the outline panel. /// /// Default: true pub git_status: Option, /// 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. /// Gitignored entries are never auto revealed. /// /// Default: true pub auto_reveal_entries: Option, /// Whether to fold directories automatically /// when directory has only one directory inside. /// /// Default: true pub auto_fold_dirs: Option, /// Settings related to indent guides in the outline panel. pub indent_guides: Option, /// Scrollbar-related settings pub scrollbar: Option, /// Default depth to expand outline items in the current file. /// The default depth to which outline entries are expanded on reveal. /// - Set to 0 to collapse all items that have children /// - Set to 1 or higher to collapse items at that depth or deeper /// /// Default: 100 pub expand_outlines_with_depth: Option, } #[derive( Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom, strum::VariantArray, strum::VariantNames, )] #[serde(rename_all = "snake_case")] pub enum DockSide { Left, Right, } #[derive( Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema, MergeFrom, strum::VariantArray, strum::VariantNames, )] #[serde(rename_all = "snake_case")] pub enum ShowIndentGuides { Always, Never, } #[skip_serializing_none] #[derive( Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default, )] pub struct IndentGuidesSettingsContent { /// When to show the scrollbar in the outline panel. pub show: Option, } #[derive(Clone, Copy, Default, PartialEq, Debug, JsonSchema, MergeFrom, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum LineIndicatorFormat { Short, #[default] Long, } /// The settings for the image viewer. #[skip_serializing_none] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, Default, PartialEq)] pub struct ImageViewerSettingsContent { /// The unit to use for displaying image file sizes. /// /// Default: "binary" pub unit: Option, } #[skip_serializing_none] #[derive( Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, Default, PartialEq, strum::VariantArray, strum::VariantNames, )] #[serde(rename_all = "snake_case")] pub enum ImageFileSizeUnit { /// Displays file size in binary units (e.g., KiB, MiB). #[default] Binary, /// Displays file size in decimal units (e.g., KB, MB). Decimal, } #[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] pub struct RemoteSettingsContent { pub ssh_connections: Option>, pub wsl_connections: Option>, pub read_ssh_config: Option, } #[skip_serializing_none] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] pub struct SshConnection { pub host: SharedString, pub username: Option, pub port: Option, #[serde(default)] pub args: Vec, #[serde(default)] pub projects: collections::BTreeSet, /// Name to use for this server in UI. pub nickname: Option, // By default Zed will download the binary to the host directly. // If this is set to true, Zed will download the binary to your local machine, // and then upload it over the SSH connection. Useful if your SSH server has // limited outbound internet access. pub upload_binary_over_ssh: Option, pub port_forwards: Option>, } #[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Debug)] pub struct WslConnection { pub distro_name: SharedString, pub user: Option, #[serde(default)] pub projects: BTreeSet, } #[skip_serializing_none] #[derive( Clone, Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema, )] pub struct SshProject { pub paths: Vec, } #[skip_serializing_none] #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, MergeFrom)] pub struct SshPortForwardOption { #[serde(skip_serializing_if = "Option::is_none")] pub local_host: Option, pub local_port: u16, #[serde(skip_serializing_if = "Option::is_none")] pub remote_host: Option, pub remote_port: u16, } /// Settings for configuring REPL display and behavior. #[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] pub struct ReplSettingsContent { /// Maximum number of lines to keep in REPL's scrollback buffer. /// Clamped with [4, 256] range. /// /// Default: 32 pub max_lines: Option, /// Maximum number of columns to keep in REPL's scrollback buffer. /// Clamped with [20, 512] range. /// /// Default: 128 pub max_columns: Option, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] /// An ExtendingVec in the settings can only accumulate new values. /// /// This is useful for things like private files where you only want /// to allow new values to be added. /// /// Consider using a HashMap instead of this type /// (like auto_install_extensions) so that user settings files can both add /// and remove values from the set. pub struct ExtendingVec(pub Vec); impl Into> for ExtendingVec { fn into(self) -> Vec { self.0 } } impl From> for ExtendingVec { fn from(vec: Vec) -> Self { ExtendingVec(vec) } } impl merge_from::MergeFrom for ExtendingVec { fn merge_from(&mut self, other: &Self) { self.0.extend_from_slice(other.0.as_slice()); } } /// A SaturatingBool in the settings can only ever be set to true, /// later attempts to set it to false will be ignored. /// /// Used by `disable_ai`. #[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct SaturatingBool(pub bool); impl From for SaturatingBool { fn from(value: bool) -> Self { SaturatingBool(value) } } impl From for bool { fn from(value: SaturatingBool) -> bool { value.0 } } impl merge_from::MergeFrom for SaturatingBool { fn merge_from(&mut self, other: &Self) { self.0 |= other.0 } } #[derive( Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, MergeFrom, JsonSchema, derive_more::FromStr, )] #[serde(transparent)] pub struct DelayMs(pub u64); impl From for DelayMs { fn from(n: u64) -> Self { Self(n) } } impl std::fmt::Display for DelayMs { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}ms", self.0) } } /// A wrapper type that distinguishes between an explicitly set value (including null) and an unset value. /// /// This is useful for configuration where you need to differentiate between: /// - A field that is not present in the configuration file (`Maybe::Unset`) /// - A field that is explicitly set to `null` (`Maybe::Set(None)`) /// - A field that is explicitly set to a value (`Maybe::Set(Some(value))`) /// /// # Examples /// /// In JSON: /// - `{}` (field missing) deserializes to `Maybe::Unset` /// - `{"field": null}` deserializes to `Maybe::Set(None)` /// - `{"field": "value"}` deserializes to `Maybe::Set(Some("value"))` /// /// WARN: This type should not be wrapped in an option inside of settings, otherwise the default `serde_json` behavior /// of treating `null` and missing as the `Option::None` will be used #[derive(Debug, Clone, PartialEq, Eq, strum::EnumDiscriminants, Default)] #[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] pub enum Maybe { /// An explicitly set value, which may be `None` (representing JSON `null`) or `Some(value)`. Set(Option), /// A value that was not present in the configuration. #[default] Unset, } impl merge_from::MergeFrom for Maybe { fn merge_from(&mut self, other: &Self) { if self.is_unset() { *self = other.clone(); } } } impl From>> for Maybe { fn from(value: Option>) -> Self { match value { Some(value) => Maybe::Set(value), None => Maybe::Unset, } } } impl Maybe { pub fn is_set(&self) -> bool { matches!(self, Maybe::Set(_)) } pub fn is_unset(&self) -> bool { matches!(self, Maybe::Unset) } pub fn into_inner(self) -> Option { match self { Maybe::Set(value) => value, Maybe::Unset => None, } } pub fn as_ref(&self) -> Option<&Option> { match self { Maybe::Set(value) => Some(value), Maybe::Unset => None, } } } impl serde::Serialize for Maybe { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { match self { Maybe::Set(value) => value.serialize(serializer), Maybe::Unset => serializer.serialize_none(), } } } impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Maybe { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { Option::::deserialize(deserializer).map(Maybe::Set) } } impl JsonSchema for Maybe { fn schema_name() -> std::borrow::Cow<'static, str> { format!("Nullable<{}>", T::schema_name()).into() } fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { let mut schema = generator.subschema_for::>(); // Add description explaining that null is an explicit value let description = if let Some(existing_desc) = schema.get("description").and_then(|desc| desc.as_str()) { format!( "{}. Note: `null` is treated as an explicit value, different from omitting the field entirely.", existing_desc ) } else { "This field supports explicit `null` values. Omitting the field is different from setting it to `null`.".to_string() }; schema.insert("description".to_string(), description.into()); schema } } #[cfg(test)] mod tests { use super::*; use serde_json; #[test] fn test_maybe() { #[derive(Debug, PartialEq, Serialize, Deserialize)] struct TestStruct { #[serde(default)] #[serde(skip_serializing_if = "Maybe::is_unset")] field: Maybe, } #[derive(Debug, PartialEq, Serialize, Deserialize)] struct NumericTest { #[serde(default)] value: Maybe, } let json = "{}"; let result: TestStruct = serde_json::from_str(json).unwrap(); assert!(result.field.is_unset()); assert_eq!(result.field, Maybe::Unset); let json = r#"{"field": null}"#; let result: TestStruct = serde_json::from_str(json).unwrap(); assert!(result.field.is_set()); assert_eq!(result.field, Maybe::Set(None)); let json = r#"{"field": "hello"}"#; let result: TestStruct = serde_json::from_str(json).unwrap(); assert!(result.field.is_set()); assert_eq!(result.field, Maybe::Set(Some("hello".to_string()))); let test = TestStruct { field: Maybe::Unset, }; let json = serde_json::to_string(&test).unwrap(); assert_eq!(json, "{}"); let test = TestStruct { field: Maybe::Set(None), }; let json = serde_json::to_string(&test).unwrap(); assert_eq!(json, r#"{"field":null}"#); let test = TestStruct { field: Maybe::Set(Some("world".to_string())), }; let json = serde_json::to_string(&test).unwrap(); assert_eq!(json, r#"{"field":"world"}"#); let default_maybe: Maybe = Maybe::default(); assert!(default_maybe.is_unset()); let unset: Maybe = Maybe::Unset; assert!(unset.is_unset()); assert!(!unset.is_set()); let set_none: Maybe = Maybe::Set(None); assert!(set_none.is_set()); assert!(!set_none.is_unset()); let set_some: Maybe = Maybe::Set(Some("value".to_string())); assert!(set_some.is_set()); assert!(!set_some.is_unset()); let original = TestStruct { field: Maybe::Set(Some("test".to_string())), }; let json = serde_json::to_string(&original).unwrap(); let deserialized: TestStruct = serde_json::from_str(&json).unwrap(); assert_eq!(original, deserialized); let json = r#"{"value": 42}"#; let result: NumericTest = serde_json::from_str(json).unwrap(); assert_eq!(result.value, Maybe::Set(Some(42))); let json = r#"{"value": null}"#; let result: NumericTest = serde_json::from_str(json).unwrap(); assert_eq!(result.value, Maybe::Set(None)); let json = "{}"; let result: NumericTest = serde_json::from_str(json).unwrap(); assert_eq!(result.value, Maybe::Unset); // Test JsonSchema implementation use schemars::schema_for; let schema = schema_for!(Maybe); let schema_json = serde_json::to_value(&schema).unwrap(); // Verify the description mentions that null is an explicit value let description = schema_json["description"].as_str().unwrap(); assert!( description.contains("null") && description.contains("explicit"), "Schema description should mention that null is an explicit value. Got: {}", description ); } }