use serde::Deserialize;
use serde::de::{self, Deserializer, Visitor};
const DEFAULT_INDENT_WIDTH: usize = 2;
const DEFAULT_TAB_WIDTH: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutoreloadMode {
None,
Replace,
Merge,
}
impl<'de> Deserialize<'de> for AutoreloadMode {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct ModeVisitor;
impl Visitor<'_> for ModeVisitor {
type Value = AutoreloadMode;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str(r#"a boolean or one of "none", "replace", "merge""#)
}
fn visit_bool<E: de::Error>(self, b: bool) -> Result<AutoreloadMode, E> {
Ok(if b {
AutoreloadMode::Replace
} else {
AutoreloadMode::None
})
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<AutoreloadMode, E> {
match s {
"none" | "off" => Ok(AutoreloadMode::None),
"replace" | "prompt" => Ok(AutoreloadMode::Replace),
"merge" => Ok(AutoreloadMode::Merge),
other => Err(de::Error::unknown_variant(
other,
&["none", "replace", "merge"],
)),
}
}
}
d.deserialize_any(ModeVisitor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
pub enum IndentGuideStyle {
#[serde(rename = "line")]
Line,
#[serde(rename = "p10k")]
P10k,
}
#[derive(Debug, Default, Clone, Deserialize)]
pub struct EditorToml {
pub indent_width: Option<usize>,
pub tab_width: Option<usize>,
pub use_tabs: Option<bool>,
pub show_whitespace: Option<bool>,
pub format_on_save: Option<bool>,
pub autoreload: Option<AutoreloadMode>,
pub indent_guides: Option<bool>,
pub indent_guides_skip_levels: Option<usize>,
pub indent_guide_style: Option<IndentGuideStyle>,
pub indent_animation: Option<bool>,
pub indent_animation_ms: Option<u64>,
pub compact_folders: Option<bool>,
}
#[derive(Debug, Clone, Copy)]
pub struct EditorConfig {
pub indent_width: usize,
pub tab_width: usize,
pub use_tabs: bool,
pub show_whitespace: bool,
pub format_on_save: bool,
pub autoreload: AutoreloadMode,
pub indent_guides: bool,
pub indent_guides_skip_levels: usize,
pub indent_guide_style: IndentGuideStyle,
pub indent_animation: bool,
pub indent_animation_ms: u64,
pub compact_folders: bool,
}
impl Default for EditorConfig {
fn default() -> Self {
Self {
indent_width: DEFAULT_INDENT_WIDTH,
tab_width: DEFAULT_TAB_WIDTH,
use_tabs: false,
show_whitespace: false,
format_on_save: true,
autoreload: AutoreloadMode::Replace,
indent_guides: true,
indent_guides_skip_levels: 0,
indent_guide_style: IndentGuideStyle::Line,
indent_animation: false,
indent_animation_ms: 100,
compact_folders: true,
}
}
}
impl EditorConfig {
pub fn overlay(self, user: &EditorToml) -> Self {
Self {
indent_width: user.indent_width.unwrap_or(self.indent_width),
tab_width: user.tab_width.unwrap_or(self.tab_width),
use_tabs: user.use_tabs.unwrap_or(self.use_tabs),
show_whitespace: user.show_whitespace.unwrap_or(self.show_whitespace),
format_on_save: user.format_on_save.unwrap_or(self.format_on_save),
autoreload: user.autoreload.unwrap_or(self.autoreload),
indent_guides: user.indent_guides.unwrap_or(self.indent_guides),
indent_guides_skip_levels: user
.indent_guides_skip_levels
.unwrap_or(self.indent_guides_skip_levels),
indent_guide_style: user.indent_guide_style.unwrap_or(self.indent_guide_style),
indent_animation: user.indent_animation.unwrap_or(self.indent_animation),
indent_animation_ms: user.indent_animation_ms.unwrap_or(self.indent_animation_ms),
compact_folders: user.compact_folders.unwrap_or(self.compact_folders),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_when_user_omits_fields() {
let eff = EditorConfig::default().overlay(&EditorToml::default());
assert_eq!(eff.indent_width, 2);
assert_eq!(eff.tab_width, 4);
}
#[test]
fn overlay_replaces_only_provided_fields() {
let base = EditorConfig {
indent_width: 4,
tab_width: 4,
use_tabs: false,
show_whitespace: false,
format_on_save: true,
autoreload: AutoreloadMode::Replace,
indent_guides: true,
indent_guides_skip_levels: 0,
indent_guide_style: IndentGuideStyle::Line,
indent_animation: false,
indent_animation_ms: 150,
compact_folders: true,
};
let eff = base.overlay(&EditorToml {
tab_width: Some(8),
..Default::default()
});
assert_eq!(eff.indent_width, 4);
assert_eq!(eff.tab_width, 8);
}
fn parse_autoreload(toml_src: &str) -> Option<AutoreloadMode> {
toml::from_str::<EditorToml>(toml_src).unwrap().autoreload
}
#[test]
fn autoreload_accepts_legacy_booleans() {
assert_eq!(
parse_autoreload("autoreload = true"),
Some(AutoreloadMode::Replace)
);
assert_eq!(
parse_autoreload("autoreload = false"),
Some(AutoreloadMode::None)
);
}
#[test]
fn autoreload_accepts_mode_strings() {
assert_eq!(
parse_autoreload(r#"autoreload = "none""#),
Some(AutoreloadMode::None)
);
assert_eq!(
parse_autoreload(r#"autoreload = "replace""#),
Some(AutoreloadMode::Replace)
);
assert_eq!(
parse_autoreload(r#"autoreload = "merge""#),
Some(AutoreloadMode::Merge)
);
}
#[test]
fn autoreload_rejects_unknown_string() {
assert!(toml::from_str::<EditorToml>(r#"autoreload = "sometimes""#).is_err());
}
}