Skip to main content

bynkc_lsp/
project.rs

1//! Bynk project configuration (`bynk.toml`).
2//!
3//! Parses the project's `bynk.toml` if one exists at the project root. All
4//! fields have sensible defaults so an absent or minimal config is fine.
5
6use std::path::Path;
7
8use bynk_fmt::{FormatOptions, IndentStyle};
9use serde::Deserialize;
10
11#[derive(Debug, Deserialize, Default)]
12struct RawConfig {
13    #[serde(default)]
14    project: ProjectSection,
15    #[serde(default)]
16    paths: PathsSection,
17    #[serde(default)]
18    fmt: FmtSection,
19    #[serde(default)]
20    lsp: LspSection,
21}
22
23#[derive(Debug, Deserialize, Default, Clone)]
24struct ProjectSection {
25    #[serde(default)]
26    pub name: Option<String>,
27    #[serde(default)]
28    pub version: Option<String>,
29}
30
31#[derive(Debug, Deserialize, Clone)]
32struct PathsSection {
33    // v0.113 (DECISION S): flat `include`/`exclude` layout. The legacy
34    // role-named `src`/`tests` keys are gone; an old config that still carries
35    // them is tolerated (unknown keys are ignored) and falls back to defaults.
36    #[serde(default = "default_include")]
37    pub include: Vec<String>,
38    // Parsed for round-trip fidelity; the LSP's analyse walk does not yet prune
39    // by `exclude` (the compiler's discovery does).
40    #[serde(default)]
41    #[allow(dead_code)]
42    pub exclude: Vec<String>,
43    #[serde(default = "default_out")]
44    pub out: String,
45}
46
47impl Default for PathsSection {
48    fn default() -> Self {
49        Self {
50            include: default_include(),
51            exclude: Vec::new(),
52            out: default_out(),
53        }
54    }
55}
56
57fn default_include() -> Vec<String> {
58    vec!["src".into()]
59}
60fn default_out() -> String {
61    "out".into()
62}
63
64#[derive(Debug, Deserialize, Clone)]
65struct FmtSection {
66    #[serde(default = "default_indent")]
67    pub indent: String,
68    #[serde(default)]
69    pub indent_width: Option<u8>,
70    #[serde(default = "default_max_line_width")]
71    pub max_line_width: u32,
72    #[serde(default = "default_trailing_comma")]
73    pub trailing_comma: bool,
74}
75
76impl Default for FmtSection {
77    fn default() -> Self {
78        Self {
79            indent: default_indent(),
80            indent_width: None,
81            max_line_width: default_max_line_width(),
82            trailing_comma: default_trailing_comma(),
83        }
84    }
85}
86
87fn default_indent() -> String {
88    "tab".into()
89}
90fn default_max_line_width() -> u32 {
91    100
92}
93fn default_trailing_comma() -> bool {
94    true
95}
96
97#[derive(Debug, Deserialize, Clone)]
98struct LspSection {
99    #[serde(default = "default_diagnostics_mode")]
100    pub diagnostics_mode: String,
101    #[serde(default = "default_diagnostics_debounce_ms")]
102    pub diagnostics_debounce_ms: u64,
103}
104
105impl Default for LspSection {
106    fn default() -> Self {
107        Self {
108            diagnostics_mode: default_diagnostics_mode(),
109            diagnostics_debounce_ms: default_diagnostics_debounce_ms(),
110        }
111    }
112}
113
114fn default_diagnostics_mode() -> String {
115    "live".into()
116}
117fn default_diagnostics_debounce_ms() -> u64 {
118    300
119}
120
121/// Effective project configuration with all defaults resolved.
122#[derive(Debug, Clone)]
123pub struct ProjectConfig {
124    #[allow(dead_code)]
125    pub project_name: Option<String>,
126    #[allow(dead_code)]
127    pub project_version: Option<String>,
128    pub src_dir: String,
129    #[allow(dead_code)]
130    pub out_dir: String,
131    pub indent: IndentStyle,
132    pub max_line_width: u32,
133    pub trailing_comma: bool,
134    #[allow(dead_code)]
135    pub diagnostics_mode: DiagnosticsMode,
136    pub diagnostics_debounce_ms: u64,
137}
138
139impl Default for ProjectConfig {
140    fn default() -> Self {
141        Self {
142            project_name: None,
143            project_version: None,
144            src_dir: "src".into(),
145            out_dir: "out".into(),
146            indent: IndentStyle::Tab,
147            max_line_width: 100,
148            trailing_comma: true,
149            diagnostics_mode: DiagnosticsMode::Live,
150            diagnostics_debounce_ms: 300,
151        }
152    }
153}
154
155impl ProjectConfig {
156    pub fn format_options(&self) -> FormatOptions {
157        FormatOptions {
158            indent: self.indent,
159            max_line_width: self.max_line_width,
160            trailing_comma: self.trailing_comma,
161        }
162    }
163}
164
165#[allow(dead_code)]
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum DiagnosticsMode {
168    Live,
169    OnSave,
170}
171
172/// Load `bynk.toml` from the given project root.
173pub fn load_config(root: &Path) -> Option<ProjectConfig> {
174    let path = root.join("bynk.toml");
175    let source = std::fs::read_to_string(&path).ok()?;
176    let raw: RawConfig = toml::from_str(&source).ok()?;
177    let indent = match raw.fmt.indent.as_str() {
178        "tab" => IndentStyle::Tab,
179        "spaces" => IndentStyle::Spaces(raw.fmt.indent_width.unwrap_or(2)),
180        _ => IndentStyle::Tab,
181    };
182    let diagnostics_mode = match raw.lsp.diagnostics_mode.as_str() {
183        "on_save" => DiagnosticsMode::OnSave,
184        _ => DiagnosticsMode::Live,
185    };
186    Some(ProjectConfig {
187        project_name: raw.project.name,
188        project_version: raw.project.version,
189        // The primary `include` tree is the source root used for cross-file
190        // lookups (defaults to `src`).
191        src_dir: raw
192            .paths
193            .include
194            .first()
195            .cloned()
196            .unwrap_or_else(|| "src".into()),
197        out_dir: raw.paths.out,
198        indent,
199        max_line_width: raw.fmt.max_line_width,
200        trailing_comma: raw.fmt.trailing_comma,
201        diagnostics_mode,
202        diagnostics_debounce_ms: raw.lsp.diagnostics_debounce_ms,
203    })
204}