1extern 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() -> Result<(), tatara_lisp::domain::KeywordCollision> {
129 tatara_lisp::domain::register::<Self>()?;
130 tatara_lisp::domain::register::<KeymapDecl>()?;
131 tatara_lisp::domain::register::<CommandDecl>()?;
132 tatara_lisp::domain::register::<PluginDecl>()?;
133 tatara_lisp::domain::register::<MajorMode>()?;
134 tatara_lisp::domain::register::<MinorMode>()?;
135 Ok(())
136 }
137}
138
139impl shikumi::TieredConfig for EscribaConfig {
156 fn bare() -> Self {
158 Self {
159 tema: None,
160 numeros_linha: None,
161 numeros_relativos: None,
162 largura_tab: None,
163 quebra_suave: None,
164 mostrar_statusline: None,
165 mostrar_tabbar: None,
166 }
167 }
168
169 fn prescribed_default() -> Self {
183 Self {
184 tema: Some(
185 ishou_tokens::FleetTheme::prescribed_default()
186 .preset_name()
187 .to_string(),
188 ),
189 numeros_linha: Some(true),
190 numeros_relativos: Some(true),
191 largura_tab: Some(2),
192 quebra_suave: Some(false),
193 mostrar_statusline: Some(true),
194 mostrar_tabbar: Some(false),
195 }
196 }
197}
198
199#[cfg(test)]
200mod tiered_tests {
201 use super::*;
202 use shikumi::{ConfigTier, TieredConfig};
203
204 #[test]
205 fn escriba_config_bare_is_zero_opinion() {
206 let b = <EscribaConfig as TieredConfig>::bare();
207 assert!(b.tema.is_none());
208 assert!(b.numeros_linha.is_none());
209 assert!(b.numeros_relativos.is_none());
210 assert!(b.largura_tab.is_none());
211 assert!(b.quebra_suave.is_none());
212 assert!(b.mostrar_statusline.is_none());
213 assert!(b.mostrar_tabbar.is_none());
214 }
215
216 #[test]
217 fn escriba_config_prescribed_mirrors_blnvim_baseline() {
218 let p = <EscribaConfig as TieredConfig>::prescribed_default();
221 assert_eq!(
225 p.tema.as_deref(),
226 Some(ishou_tokens::FleetTheme::prescribed_default().preset_name()),
227 );
228 assert_eq!(p.numeros_linha, Some(true));
229 assert_eq!(p.numeros_relativos, Some(true));
230 assert_eq!(p.largura_tab, Some(2));
231 assert_eq!(p.quebra_suave, Some(false));
232 assert_eq!(p.mostrar_statusline, Some(true));
233 assert_eq!(p.mostrar_tabbar, Some(false));
234 let bare = <EscribaConfig as TieredConfig>::bare();
236 assert_ne!(p, bare);
237 }
238
239 #[test]
240 fn escriba_config_resolve_tier_dispatches() {
241 let bare = <EscribaConfig as TieredConfig>::resolve_tier(ConfigTier::Bare);
243 let default = <EscribaConfig as TieredConfig>::resolve_tier(ConfigTier::Default);
244 assert_eq!(bare, <EscribaConfig as TieredConfig>::bare());
245 assert_eq!(
246 default,
247 <EscribaConfig as TieredConfig>::prescribed_default()
248 );
249 assert_eq!(
250 default.tema.as_deref(),
251 Some(ishou_tokens::FleetTheme::prescribed_default().preset_name()),
252 );
253 }
254
255 #[test]
263 fn every_statement_of_the_default_theme_agrees() {
264 let fleet = ishou_tokens::FleetTheme::prescribed_default();
265 let from_config = <EscribaConfig as TieredConfig>::prescribed_default()
266 .tema
267 .expect("the prescribed tier names a theme");
268 assert_eq!(
269 from_config,
270 fleet.preset_name(),
271 "the tiered config must name the fleet-prescribed theme",
272 );
273
274 let lisp = include_str!("../../escriba/configs/blnvim-defaults.lisp");
278 let declared = lisp
279 .lines()
280 .find_map(|l| {
281 let l = l.trim();
282 l.strip_prefix("(deftheme :preset ")
283 .map(|r| r.trim_end_matches(')').trim().trim_matches('"').to_string())
284 })
285 .expect("the shipped defaults declare a theme");
286 assert_eq!(
287 declared,
288 fleet.preset_name(),
289 "configs/blnvim-defaults.lisp declares a different theme than the \
290 one escriba reports as its default",
291 );
292 }
293
294 #[test]
295 fn escriba_config_diff_against_self_is_empty() {
296 let p = <EscribaConfig as TieredConfig>::prescribed_default();
299 assert!(p.diff_against(&p).is_empty_diff());
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn parses_defescriba() {
309 let src = r#"(defescriba :tema "nord" :numeros-linha #t :largura-tab 2)"#;
310 let c = EscribaConfig::from_lisp(src).unwrap();
311 assert_eq!(c.tema.as_deref(), Some("nord"));
312 assert_eq!(c.numeros_linha, Some(true));
313 assert_eq!(c.largura_tab, Some(2));
314 }
315
316 #[test]
317 fn parses_defkeymap() {
318 use tatara_lisp::domain::TataraDomain;
319 let forms = tatara_lisp::read(
320 r#"(defkeymap :modo "Normal" :tecla "<leader>w" :comando "save" :descricao "save")"#,
321 )
322 .unwrap();
323 let k = KeymapDecl::compile_from_sexp(&forms[0]).unwrap();
324 assert_eq!(k.modo, "Normal");
325 assert_eq!(k.comando, "save");
326 }
327
328 #[test]
329 fn parses_defmajor_mode_with_structural_lisp() {
330 use tatara_lisp::domain::TataraDomain;
331 let forms = tatara_lisp::read(
332 r#"(defmajor-mode :nome "lisp" :extensoes ("lisp" "el" "clj") :estrutural-lisp #t)"#,
333 )
334 .unwrap();
335 let m = MajorMode::compile_from_sexp(&forms[0]).unwrap();
336 assert_eq!(m.nome, "lisp");
337 assert_eq!(m.estrutural_lisp, Some(true));
338 }
339
340 #[test]
341 fn register_all_populates_registry() {
342 EscribaConfig::register_all().expect("escriba's own keywords must not collide");
343 let kws = tatara_lisp::domain::registered_keywords();
344 for keyword in [
345 "defescriba",
346 "defkeymap",
347 "defcommand",
348 "defplugin",
349 "defmajor-mode",
350 "defminor-mode",
351 ] {
352 assert!(kws.contains(&keyword), "missing keyword: {keyword}");
353 }
354 }
355
356 #[test]
357 fn registering_twice_is_idempotent_not_a_collision() {
358 EscribaConfig::register_all().expect("first");
362 EscribaConfig::register_all().expect("a repeat call is idempotent");
363 }
364
365 #[test]
373 fn a_second_type_claiming_an_escriba_keyword_is_refused() {
374 use tatara_lisp::domain::TataraDomain;
375
376 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
377 struct Impostor {
378 nome: String,
379 }
380 impl TataraDomain for Impostor {
381 const KEYWORD: &'static str = "defkeymap"; fn compile_from_args(
383 _args: &[tatara_lisp::Sexp],
384 ) -> Result<Self, tatara_lisp::LispError> {
385 Ok(Self {
386 nome: String::new(),
387 })
388 }
389 }
390
391 EscribaConfig::register_all().expect("escriba's own keywords register");
392 let err = tatara_lisp::domain::register::<Impostor>()
393 .expect_err("a different type must NOT be allowed to take `defkeymap`");
394 assert_eq!(err.keyword, "defkeymap");
395 assert!(
396 err.challenger.contains("Impostor"),
397 "the refusal must name who was turned away: {err}",
398 );
399 }
400}