use std::path::Path;
use bynk_fmt::{FmtConfig, FormatOptions, IndentStyle};
use serde::Deserialize;
#[derive(Debug, Deserialize, Default)]
struct RawConfig {
#[serde(default)]
lsp: LspSection,
}
#[derive(Debug, Deserialize, Clone)]
struct LspSection {
#[serde(default = "default_diagnostics_mode")]
pub diagnostics_mode: String,
#[serde(default = "default_diagnostics_debounce_ms")]
pub diagnostics_debounce_ms: u64,
}
impl Default for LspSection {
fn default() -> Self {
Self {
diagnostics_mode: default_diagnostics_mode(),
diagnostics_debounce_ms: default_diagnostics_debounce_ms(),
}
}
}
fn default_diagnostics_mode() -> String {
"live".into()
}
fn default_diagnostics_debounce_ms() -> u64 {
300
}
#[derive(Debug, Clone)]
pub struct ProjectConfig {
pub indent: IndentStyle,
pub max_line_width: u32,
pub trailing_comma: bool,
pub diagnostics_mode: DiagnosticsMode,
pub diagnostics_debounce_ms: u64,
}
impl Default for ProjectConfig {
fn default() -> Self {
Self {
indent: IndentStyle::Tab,
max_line_width: 100,
trailing_comma: true,
diagnostics_mode: DiagnosticsMode::Live,
diagnostics_debounce_ms: 300,
}
}
}
impl ProjectConfig {
pub fn format_options(&self) -> FormatOptions {
FormatOptions {
indent: self.indent,
max_line_width: self.max_line_width,
trailing_comma: self.trailing_comma,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticsMode {
Live,
OnSave,
}
pub fn load_config(root: &Path) -> Option<ProjectConfig> {
let path = root.join(bynk_fmt::MANIFEST);
let source = std::fs::read_to_string(&path).ok()?;
let raw: RawConfig = toml::from_str(&source).ok()?;
let fmt = FmtConfig::from_manifest_str(&source)
.unwrap_or_default()
.apply(FormatOptions::default());
let diagnostics_mode = match raw.lsp.diagnostics_mode.as_str() {
"on_save" => DiagnosticsMode::OnSave,
_ => DiagnosticsMode::Live,
};
Some(ProjectConfig {
indent: fmt.indent,
max_line_width: fmt.max_line_width,
trailing_comma: fmt.trailing_comma,
diagnostics_mode,
diagnostics_debounce_ms: raw.lsp.diagnostics_debounce_ms,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn project(name: &str, manifest: &str) -> std::path::PathBuf {
let dir =
std::env::temp_dir().join(format!("bynk-lsp-project-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join(bynk_fmt::MANIFEST), manifest).expect("write");
dir
}
#[test]
fn the_fmt_section_still_reaches_format_on_save() {
let root = project(
"lsp-fmt-section",
"[project]\nname = \"x\"\n\n[fmt]\nindent = \"spaces\"\nindent_width = 4\nmax_line_width = 120\ntrailing_comma = false\n",
);
let opts = load_config(&root).expect("config loads").format_options();
assert_eq!(opts.indent, IndentStyle::Spaces(4));
assert_eq!(opts.max_line_width, 120);
assert!(!opts.trailing_comma);
}
#[test]
fn the_editor_and_the_cli_resolve_a_manifest_identically() {
let manifest = "[fmt]\nindent = \"spaces\"\nindent_width = 3\nmax_line_width = 60\n";
let root = project("lsp-fmt-parity", manifest);
let editor = load_config(&root).expect("config loads").format_options();
let cli = bynk_fmt::FmtConfig::from_manifest(&root.join(bynk_fmt::MANIFEST))
.expect("manifest parses")
.apply(FormatOptions::default());
assert_eq!(editor, cli, "format-on-save and `fmt` must agree");
}
#[test]
fn an_absent_fmt_section_leaves_the_canonical_style() {
let root = project("lsp-fmt-absent", "[project]\nname = \"x\"\n");
let opts = load_config(&root).expect("config loads").format_options();
assert_eq!(opts, FormatOptions::default());
}
#[test]
fn a_broken_fmt_section_does_not_take_the_rest_of_the_config_with_it() {
let root = project(
"lsp-fmt-broken",
"[fmt]\nindent = \"tabs\"\n\n[lsp]\ndiagnostics_debounce_ms = 900\n",
);
let config = load_config(&root).expect("config loads");
assert_eq!(config.format_options(), FormatOptions::default());
assert_eq!(config.diagnostics_debounce_ms, 900, "[lsp] still applies");
}
}