Skip to main content

escriba_config/
lib.rs

1//! `escriba-config` — tatara-lisp editor config. Every top-level config
2//! form is a TataraDomain: `defescriba` / `defkeymap` / `defcommand` /
3//! `defplugin` / `defmajor-mode` / `defminor-mode`.
4
5extern crate self as escriba_config;
6
7use serde::{Deserialize, Serialize};
8use tatara_lisp::DeriveTataraDomain;
9
10#[derive(
11    DeriveTataraDomain,
12    Serialize,
13    Deserialize,
14    schemars::JsonSchema,
15    Debug,
16    Clone,
17    PartialEq,
18    Default,
19)]
20#[serde(rename_all = "camelCase")]
21#[tatara(keyword = "defescriba")]
22pub struct EscribaConfig {
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub tema: Option<String>,
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub numeros_linha: Option<bool>,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub numeros_relativos: Option<bool>,
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub largura_tab: Option<i64>,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub quebra_suave: Option<bool>,
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub mostrar_statusline: Option<bool>,
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub mostrar_tabbar: Option<bool>,
37}
38
39#[derive(
40    DeriveTataraDomain, Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, PartialEq,
41)]
42#[serde(rename_all = "camelCase")]
43#[tatara(keyword = "defkeymap")]
44pub struct KeymapDecl {
45    pub modo: String,
46    pub tecla: String,
47    pub comando: String,
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub descricao: Option<String>,
50}
51
52#[derive(
53    DeriveTataraDomain, Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, PartialEq,
54)]
55#[serde(rename_all = "camelCase")]
56#[tatara(keyword = "defcommand")]
57pub struct CommandDecl {
58    pub nome: String,
59    pub descricao: String,
60    #[serde(default)]
61    pub args: Vec<String>,
62}
63
64#[derive(
65    DeriveTataraDomain, Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, PartialEq,
66)]
67#[serde(rename_all = "camelCase")]
68#[tatara(keyword = "defplugin")]
69pub struct PluginDecl {
70    pub caixa: String,
71    pub versao: String,
72    #[serde(default)]
73    pub ativar_em: Vec<String>,
74}
75
76#[derive(
77    DeriveTataraDomain, Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, PartialEq,
78)]
79#[serde(rename_all = "camelCase")]
80#[tatara(keyword = "defmajor-mode")]
81pub struct MajorMode {
82    pub nome: String,
83    #[serde(default)]
84    pub extensoes: Vec<String>,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub estrutural_lisp: Option<bool>,
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub tamanho_indent: Option<i64>,
89}
90
91#[derive(
92    DeriveTataraDomain, Serialize, Deserialize, schemars::JsonSchema, Debug, Clone, PartialEq,
93)]
94#[serde(rename_all = "camelCase")]
95#[tatara(keyword = "defminor-mode")]
96pub struct MinorMode {
97    pub nome: String,
98    #[serde(default)]
99    pub hooks: Vec<String>,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub descricao: Option<String>,
102}
103
104impl EscribaConfig {
105    pub fn from_lisp(src: &str) -> Result<Self, tatara_lisp::LispError> {
106        use tatara_lisp::domain::TataraDomain;
107        let forms = tatara_lisp::read(src)?;
108        let first = forms
109            .first()
110            .ok_or_else(|| tatara_lisp::LispError::Compile {
111                form: "defescriba".into(),
112                message: "empty config".into(),
113            })?;
114        Self::compile_from_sexp(first)
115    }
116
117    pub fn register_all() {
118        tatara_lisp::domain::register::<Self>();
119        tatara_lisp::domain::register::<KeymapDecl>();
120        tatara_lisp::domain::register::<CommandDecl>();
121        tatara_lisp::domain::register::<PluginDecl>();
122        tatara_lisp::domain::register::<MajorMode>();
123        tatara_lisp::domain::register::<MinorMode>();
124    }
125}
126
127// ── shikumi::TieredConfig — fleet-wide tier model (M-166 backfill) ──
128//
129// Operators reach via:
130//   ESCRIBA_TIER=bare escriba ...
131//   ESCRIBA_TIER=default escriba ...
132//
133// Prior migrations: tatara, zoekt-mcp, kindling, ayatsuri, kenshi,
134// taimen. See `shikumi/src/tiered.rs` for the trait contract.
135//
136// bare() is the all-None zero-opinion floor. prescribed_default() now
137// mirrors the shipped `configs/blnvim-defaults.lisp` baseline (theme +
138// numbers + tab-width 2 + statusline) so `escriba config-show default`
139// reports what actually boots. The `.lisp` remains the load-bearing
140// prescription (it carries the keymaps/modes/highlights this 7-field
141// struct cannot express); this struct is the operator-facing summary.
142
143impl shikumi::TieredConfig for EscribaConfig {
144    /// Tier 0 — bare: zero-opinion floor. Every field None.
145    fn bare() -> Self {
146        Self {
147            tema: None,
148            numeros_linha: None,
149            numeros_relativos: None,
150            largura_tab: None,
151            quebra_suave: None,
152            mostrar_statusline: None,
153            mostrar_tabbar: None,
154        }
155    }
156
157    /// Tier 2 — prescribed: the curated defaults that ship today. These
158    /// MIRROR the load-bearing `configs/blnvim-defaults.lisp` baseline the
159    /// editor actually boots (theme `vellum` = `ishou_tokens::FleetTheme::Vellum`;
160    /// line numbers + relative numbers on; tab width 2; no soft wrap;
161    /// statusline on; tabbar off — `showtabline=0`, blnvim parity). The
162    /// `.lisp` remains the load-bearing prescription; this keeps
163    /// `escriba config-show default` honest about what ships.
164    fn prescribed_default() -> Self {
165        Self {
166            tema: Some("vellum".into()),
167            numeros_linha: Some(true),
168            numeros_relativos: Some(true),
169            largura_tab: Some(2),
170            quebra_suave: Some(false),
171            mostrar_statusline: Some(true),
172            mostrar_tabbar: Some(false),
173        }
174    }
175}
176
177#[cfg(test)]
178mod tiered_tests {
179    use super::*;
180    use shikumi::{ConfigTier, TieredConfig};
181
182    #[test]
183    fn escriba_config_bare_is_zero_opinion() {
184        let b = <EscribaConfig as TieredConfig>::bare();
185        assert!(b.tema.is_none());
186        assert!(b.numeros_linha.is_none());
187        assert!(b.numeros_relativos.is_none());
188        assert!(b.largura_tab.is_none());
189        assert!(b.quebra_suave.is_none());
190        assert!(b.mostrar_statusline.is_none());
191        assert!(b.mostrar_tabbar.is_none());
192    }
193
194    #[test]
195    fn escriba_config_prescribed_mirrors_blnvim_baseline() {
196        // The prescribed default mirrors the shipped blnvim-defaults.lisp so
197        // `escriba config-show default` reflects the real boot baseline.
198        let p = <EscribaConfig as TieredConfig>::prescribed_default();
199        assert_eq!(p.tema.as_deref(), Some("vellum"));
200        assert_eq!(p.numeros_linha, Some(true));
201        assert_eq!(p.numeros_relativos, Some(true));
202        assert_eq!(p.largura_tab, Some(2));
203        assert_eq!(p.quebra_suave, Some(false));
204        assert_eq!(p.mostrar_statusline, Some(true));
205        assert_eq!(p.mostrar_tabbar, Some(false));
206        // Prescribed differs from the all-None bare floor.
207        let bare = <EscribaConfig as TieredConfig>::bare();
208        assert_ne!(p, bare);
209    }
210
211    #[test]
212    fn escriba_config_resolve_tier_dispatches() {
213        // Bare is zero-opinion; Default pins the vellum theme.
214        let bare = <EscribaConfig as TieredConfig>::resolve_tier(ConfigTier::Bare);
215        let default = <EscribaConfig as TieredConfig>::resolve_tier(ConfigTier::Default);
216        assert_eq!(bare, <EscribaConfig as TieredConfig>::bare());
217        assert_eq!(default, <EscribaConfig as TieredConfig>::prescribed_default());
218        assert_eq!(default.tema.as_deref(), Some("vellum"));
219    }
220
221    #[test]
222    fn escriba_config_diff_against_self_is_empty() {
223        // The diff machinery: a value diffed against itself produces
224        // an empty diff.
225        let p = <EscribaConfig as TieredConfig>::prescribed_default();
226        assert!(p.diff_against(&p).is_empty_diff());
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn parses_defescriba() {
236        let src = r#"(defescriba :tema "nord" :numeros-linha #t :largura-tab 2)"#;
237        let c = EscribaConfig::from_lisp(src).unwrap();
238        assert_eq!(c.tema.as_deref(), Some("nord"));
239        assert_eq!(c.numeros_linha, Some(true));
240        assert_eq!(c.largura_tab, Some(2));
241    }
242
243    #[test]
244    fn parses_defkeymap() {
245        use tatara_lisp::domain::TataraDomain;
246        let forms = tatara_lisp::read(
247            r#"(defkeymap :modo "Normal" :tecla "<leader>w" :comando "save" :descricao "save")"#,
248        )
249        .unwrap();
250        let k = KeymapDecl::compile_from_sexp(&forms[0]).unwrap();
251        assert_eq!(k.modo, "Normal");
252        assert_eq!(k.comando, "save");
253    }
254
255    #[test]
256    fn parses_defmajor_mode_with_structural_lisp() {
257        use tatara_lisp::domain::TataraDomain;
258        let forms = tatara_lisp::read(
259            r#"(defmajor-mode :nome "lisp" :extensoes ("lisp" "el" "clj") :estrutural-lisp #t)"#,
260        )
261        .unwrap();
262        let m = MajorMode::compile_from_sexp(&forms[0]).unwrap();
263        assert_eq!(m.nome, "lisp");
264        assert_eq!(m.estrutural_lisp, Some(true));
265    }
266
267    #[test]
268    fn register_all_populates_registry() {
269        EscribaConfig::register_all();
270        let kws = tatara_lisp::domain::registered_keywords();
271        for keyword in [
272            "defescriba",
273            "defkeymap",
274            "defcommand",
275            "defplugin",
276            "defmajor-mode",
277            "defminor-mode",
278        ] {
279            assert!(kws.contains(&keyword), "missing keyword: {keyword}");
280        }
281    }
282}