Skip to main content

oo_ide/
settings.rs

1use std::collections::HashMap;
2
3use saphyr::{LoadableYamlNode, Yaml};
4
5pub(crate) mod adapters;
6mod value;
7
8use crate::prelude::*;
9use value::Value;
10
11pub struct Settings {
12    global_config_dir: std::path::PathBuf,
13    global_config_file: std::path::PathBuf,
14    project_config_dir: std::path::PathBuf,
15    project_config_file: std::path::PathBuf,
16    settings: HashMap<String, Value>,
17}
18
19impl Settings {
20    pub fn new(project_config_file: &std::path::Path) -> Result<Self> {
21        let global_config_dir = directories::ProjectDirs::from("com", "cyloncore", "oo")
22            .ok_or_else(|| anyhow!("Failed to retrieve project dir."))?
23            .config_dir()
24            .to_path_buf();
25        let project_config_dir = project_config_file
26            .parent()
27            .unwrap_or(project_config_file)
28            .to_path_buf();
29        let mut settings = Self {
30            global_config_file: global_config_dir.join("config.yaml"),
31            global_config_dir,
32            project_config_file: project_config_file.into(),
33            project_config_dir,
34            settings: Default::default(),
35        };
36        settings.load_settings()?;
37        Ok(settings)
38    }
39    fn load_settings(&mut self) -> Result<()> {
40        self.settings = Default::default();
41        self.load_yaml(include_str!("../data/default_settings.yaml"))?;
42        if self.global_config_file.exists() {
43            let data = std::fs::read_to_string(&self.global_config_file)?;
44            self.load_yaml(&data)?;
45        } else {
46            log::info!("No global config file {:?}", self.global_config_file);
47        }
48        if self.project_config_file.exists() {
49            let data = std::fs::read_to_string(&self.project_config_file)?;
50            self.load_yaml(&data)?;
51        } else {
52            log::info!("No project config file {:?}", self.project_config_file);
53        }
54        Ok(())
55    }
56    fn to_string<'a>(node: &'a Yaml) -> Result<&'a str> {
57        node.as_str()
58            .ok_or_else(|| anyhow!("Expected string, got {:?}", node))
59    }
60    fn load_node(&mut self, key: String, node: &saphyr::Yaml) -> Result<()> {
61        use saphyr::{Scalar, Yaml};
62        match node {
63            Yaml::Alias(_) => Err(anyhow!("aliases are not supported")),
64            Yaml::Mapping(mapping) => {
65                for (k, v) in mapping.iter() {
66                    let k = Self::to_string(k)?;
67                    self.load_node(
68                        if key.is_empty() {
69                            k.to_string()
70                        } else {
71                            format!("{}.{}", key, k)
72                        },
73                        v,
74                    )?;
75                }
76                Ok(())
77            }
78            Yaml::Representation(_, _, _) => Err(anyhow!("Unsupported representation")),
79            Yaml::Tagged(_, _) => Err(anyhow!("Unsupported tagged")),
80            Yaml::BadValue => Err(anyhow!("Bad value")),
81            Yaml::Sequence(seq) => {
82                self.settings.insert(
83                    key,
84                    Value::StringVec(
85                        seq.iter()
86                            .map(|x| Ok(Self::to_string(x)?.to_string()))
87                            .collect::<Result<_>>()?,
88                    ),
89                );
90                Ok(())
91            }
92            Yaml::Value(value) => {
93                match value {
94                    Scalar::Boolean(b) => {
95                        self.settings.insert(key, Value::Boolean(*b));
96                    }
97                    Scalar::Null => {}
98                    Scalar::Integer(i) => {
99                        self.settings.insert(key, Value::Integer(*i));
100                    }
101                    Scalar::FloatingPoint(f) => {
102                        self.settings.insert(key, Value::Float(f.into_inner()));
103                    }
104                    Scalar::String(string) => {
105                        self.settings.insert(key, Value::String(string.to_string()));
106                    }
107                }
108                Ok(())
109            }
110        }
111    }
112    fn load_yaml(&mut self, data: &str) -> Result<()> {
113        let nodes = saphyr::Yaml::load_from_str(data)?;
114        if let Some(node) = nodes.first() {
115            self.load_node("".to_string(), node)?;
116        }
117        Ok(())
118    }
119    /// Returns the path to the global config directory (`~/.config/oo/`).
120    pub fn global_config_dir(&self) -> &std::path::Path {
121        &self.global_config_dir
122    }
123
124    /// Returns the path to the project-local config directory (`.oo/`).
125    pub fn project_config_dir(&self) -> &std::path::Path {
126        &self.project_config_dir
127    }
128
129    /// Like `get`, but returns `None` instead of logging an error when the key
130    /// is missing.  Use this for truly optional settings (e.g. theme overrides).
131    pub(crate) fn get_optional<'a, T>(&'a self, key: impl AsRef<str>) -> Option<&'a T>
132    where
133        &'a T: TryFrom<&'a Value> + std::fmt::Debug,
134        T: value::ConstantDefault,
135        <&'a T as TryFrom<&'a Value>>::Error: std::fmt::Debug,
136    {
137        let key = key.as_ref();
138        let v = self.settings.get(key)?;
139        match v.try_into() {
140            Ok(v) => Some(v),
141            Err(e) => {
142                log::error!("Failed to cast '{v:?}' for key '{key}' with error: '{e:?}'");
143                None
144            }
145        }
146    }
147
148    pub(crate) fn get<'a, T>(&'a self, key: impl AsRef<str>) -> &'a T
149    where
150        &'a T: TryFrom<&'a Value> + std::fmt::Debug,
151        T: value::ConstantDefault,
152        <&'a T as TryFrom<&'a Value>>::Error: std::fmt::Debug,
153    {
154        let key = key.as_ref();
155        if let Some(v) = self.settings.get(key) {
156            match v.try_into() {
157                Ok(v) => v,
158                Err(e) => {
159                    log::error!("Failed to cast '{v:?}' for key '{key}' with error: '{e:?}'");
160                    T::constant_default()
161                }
162            }
163        } else {
164            log::error!("Unknown '{key}' key in settings, make sure to define a default.");
165            T::constant_default()
166        }
167    }
168
169    pub(crate) fn merge_extension_defaults(&mut self, defaults: Vec<(String, String)>) {
170        for (ext_name, yaml) in defaults {
171            log::debug!("Loading defaults for extension '{}'", ext_name);
172            if let Err(e) = self.load_yaml(&yaml) {
173                log::warn!("Failed to load defaults for extension '{}': {}", ext_name, e);
174            }
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    fn create_test_settings() -> (Settings, tempfile::TempDir) {
184        let dir = tempfile::tempdir().unwrap();
185        let project_config = dir.path().join(".oo").join("config.yaml");
186        std::fs::create_dir(dir.path().join(".oo")).unwrap();
187
188        let settings = Settings::new(&project_config).expect("Failed to create test settings");
189        (settings, dir)
190    }
191
192    #[test]
193    fn test_load_yaml_boolean() {
194        let (mut settings, _dir) = create_test_settings();
195        let yaml = r#"
196test_bool: true
197"#;
198        settings.load_yaml(yaml).unwrap();
199        assert!(settings.settings.contains_key("test_bool"));
200    }
201
202    #[test]
203    fn test_load_yaml_integer() {
204        let (mut settings, _dir) = create_test_settings();
205        let yaml = r#"
206test_int: 42
207"#;
208        settings.load_yaml(yaml).unwrap();
209        assert!(settings.settings.contains_key("test_int"));
210    }
211
212    #[test]
213    fn test_load_yaml_float() {
214        let (mut settings, _dir) = create_test_settings();
215        let yaml = r#"
216test_float: 3.14
217"#;
218        settings.load_yaml(yaml).unwrap();
219        assert!(settings.settings.contains_key("test_float"));
220    }
221
222    #[test]
223    fn test_load_yaml_string() {
224        let (mut settings, _dir) = create_test_settings();
225        let yaml = r#"
226test_string: "hello"
227"#;
228        settings.load_yaml(yaml).unwrap();
229        assert!(settings.settings.contains_key("test_string"));
230    }
231
232    #[test]
233    fn test_load_yaml_sequence() {
234        let (mut settings, _dir) = create_test_settings();
235        let yaml = r#"
236test_seq:
237  - item1
238  - item2
239  - item3
240"#;
241        settings.load_yaml(yaml).unwrap();
242        assert!(settings.settings.contains_key("test_seq"));
243    }
244
245    #[test]
246    fn test_load_yaml_nested_mapping() {
247        let (mut settings, _dir) = create_test_settings();
248        let yaml = r#"
249parent:
250  child1: value1
251  child2: value2
252"#;
253        settings.load_yaml(yaml).unwrap();
254        assert!(settings.settings.contains_key("parent.child1"));
255        assert!(settings.settings.contains_key("parent.child2"));
256    }
257
258    #[test]
259    fn test_load_yaml_deeply_nested() {
260        let (mut settings, _dir) = create_test_settings();
261        let yaml = r#"
262level1:
263  level2:
264    level3:
265      value: deep
266"#;
267        settings.load_yaml(yaml).unwrap();
268        assert!(settings.settings.contains_key("level1.level2.level3.value"));
269    }
270
271    #[test]
272    fn test_load_yaml_null_value() {
273        let (mut settings, _dir) = create_test_settings();
274        let yaml = r#"
275test_null:
276"#;
277        settings.load_yaml(yaml).unwrap();
278        // Null values should not be inserted
279        assert!(!settings.settings.contains_key("test_null"));
280    }
281
282    #[test]
283    fn test_load_yaml_invalid_yaml() {
284        let (mut settings, _dir) = create_test_settings();
285        let invalid_yaml = "invalid: [unclosed";
286        let result = settings.load_yaml(invalid_yaml);
287        assert!(result.is_err());
288    }
289
290    #[test]
291    fn test_load_yaml_bad_value() {
292        let (mut settings, _dir) = create_test_settings();
293        let yaml = "badval: !!binary\n  sdlkfjlskdjf";
294        let result = settings.load_yaml(yaml);
295        assert!(result.is_err());
296    }
297
298    #[test]
299    fn test_get_existing_bool() {
300        let (mut settings, _dir) = create_test_settings();
301        let yaml = "test_bool: true";
302        settings.load_yaml(yaml).unwrap();
303        let val: &bool = settings.get("test_bool");
304        assert!(*val);
305    }
306
307    #[test]
308    fn test_get_existing_integer() {
309        let (mut settings, _dir) = create_test_settings();
310        let yaml = "test_int: 100";
311        settings.load_yaml(yaml).unwrap();
312        let val: &i64 = settings.get("test_int");
313        assert_eq!(*val, 100);
314    }
315
316    #[test]
317    fn test_get_existing_float() {
318        let (mut settings, _dir) = create_test_settings();
319        let yaml = "test_float: 2.71";
320        settings.load_yaml(yaml).unwrap();
321        let val: &f64 = settings.get("test_float");
322        assert_eq!(*val, 2.71);
323    }
324
325    #[test]
326    fn test_get_existing_string() {
327        let (mut settings, _dir) = create_test_settings();
328        let yaml = "test_string: \"world\"";
329        settings.load_yaml(yaml).unwrap();
330        let val: &String = settings.get("test_string");
331        assert_eq!(*val, "world");
332    }
333
334    #[test]
335    fn test_get_existing_vec_string() {
336        let (mut settings, _dir) = create_test_settings();
337        let yaml = r#"
338test_vec:
339  - a
340  - b
341  - c
342"#;
343        settings.load_yaml(yaml).unwrap();
344        let val: &Vec<String> = settings.get("test_vec");
345        assert_eq!(val.len(), 3);
346        assert_eq!(val[0], "a");
347    }
348
349    #[test]
350    fn test_get_missing_key_returns_default() {
351        let (settings, _dir) = create_test_settings();
352        let val: &bool = settings.get("nonexistent_bool");
353        assert!(!*val);
354    }
355
356    #[test]
357    fn test_get_optional_existing_key() {
358        let (mut settings, _dir) = create_test_settings();
359        let yaml = "test_bool: true";
360        settings.load_yaml(yaml).unwrap();
361        let val: Option<&bool> = settings.get_optional("test_bool");
362        assert_eq!(val, Some(&true));
363    }
364
365    #[test]
366    fn test_get_optional_missing_key() {
367        let (settings, _dir) = create_test_settings();
368        let val: Option<&bool> = settings.get_optional("nonexistent");
369        assert_eq!(val, None);
370    }
371
372    #[test]
373    fn test_get_type_mismatch_returns_default() {
374        let (mut settings, _dir) = create_test_settings();
375        let yaml = "test_int: 42";
376        settings.load_yaml(yaml).unwrap();
377        // Try to get as bool (wrong type)
378        let val: &bool = settings.get("test_int");
379        assert!(!*val);
380    }
381
382    #[test]
383    fn test_get_optional_type_mismatch_returns_none() {
384        let (mut settings, _dir) = create_test_settings();
385        let yaml = "test_int: 42";
386        settings.load_yaml(yaml).unwrap();
387        // Try to get as bool (wrong type)
388        let val: Option<&bool> = settings.get_optional("test_int");
389        assert_eq!(val, None);
390    }
391
392    #[test]
393    fn test_global_config_dir() {
394        let (settings, _dir) = create_test_settings();
395        let dir = settings.global_config_dir();
396        assert!(dir.is_absolute() || !dir.as_os_str().is_empty());
397    }
398
399    #[test]
400    fn test_project_config_dir() {
401        let (settings, _dir) = create_test_settings();
402        let config_dir = settings.project_config_dir();
403        // Should point to the .oo directory created in the test
404        let is_oo_dir = config_dir
405            .file_name()
406            .and_then(|s| s.to_str())
407            .map(|name| name == ".oo" || name == "oo")
408            .unwrap_or(false);
409        assert!(is_oo_dir);
410    }
411
412    #[test]
413    fn test_settings_merge_multiple_loads() {
414        let (mut settings, _dir) = create_test_settings();
415        let yaml1 = "key1: value1";
416        let yaml2 = "key2: value2";
417
418        settings.load_yaml(yaml1).unwrap();
419        settings.load_yaml(yaml2).unwrap();
420
421        assert!(settings.settings.contains_key("key1"));
422        assert!(settings.settings.contains_key("key2"));
423    }
424
425    #[test]
426    fn test_load_yaml_overwrites_previous() {
427        let (mut settings, _dir) = create_test_settings();
428        let yaml1 = "same_key: 1";
429        let yaml2 = "same_key: 2";
430
431        settings.load_yaml(yaml1).unwrap();
432        let val1: &i64 = settings.get("same_key");
433        assert_eq!(*val1, 1);
434
435        settings.load_yaml(yaml2).unwrap();
436        let val2: &i64 = settings.get("same_key");
437        assert_eq!(*val2, 2);
438    }
439
440    #[test]
441    fn test_to_string_helper_valid() {
442        let (mut settings, _dir) = create_test_settings();
443        let yaml = "test: hello";
444        settings.load_yaml(yaml).unwrap();
445        assert!(settings.settings.contains_key("test"));
446    }
447
448    #[test]
449    fn test_load_yaml_empty_document() {
450        let (mut settings, _dir) = create_test_settings();
451        let yaml = "";
452        let result = settings.load_yaml(yaml);
453        assert!(result.is_ok());
454    }
455
456    // --- Language-scoped settings tests ---
457
458    #[test]
459    fn test_language_lsp_falls_back_to_global() {
460        let (settings, _dir) = create_test_settings();
461        // No language-specific config; should return global defaults.
462        // Global default has lsp.enable: false
463        assert!(!*adapters::language::lsp::enable(&settings, "rust"));
464    }
465
466    #[test]
467    fn test_language_lsp_enable_parses() {
468        let (mut settings, _dir) = create_test_settings();
469        let yaml = r#"
470languages:
471  cpp:
472    lsp:
473      enable: true
474      server_command: "clangd"
475"#;
476        settings.load_yaml(yaml).unwrap();
477
478        assert!(*adapters::language::lsp::enable(&settings, "cpp"));
479        assert_eq!(adapters::language::lsp::server_command(&settings, "cpp"), "clangd");
480    }
481
482    #[test]
483    fn test_language_lsp_quoted_true_parses() {
484        let (mut settings, _dir) = create_test_settings();
485        let yaml = r#"
486languages:
487  rust:
488    lsp:
489      enable: "true"
490      server_command: "rust-analyzer"
491"#;
492        settings.load_yaml(yaml).unwrap();
493
494        // Quoted booleans are strings and should not be coerced to bools.
495        assert!(!*adapters::language::lsp::enable(&settings, "rust"));
496        assert_eq!(adapters::language::lsp::server_command(&settings, "rust"), "rust-analyzer");
497    }
498
499    #[test]
500    fn test_language_lsp_uses_override() {
501        let (mut settings, _dir) = create_test_settings();
502        let yaml = r#"
503languages:
504  rust:
505    lsp:
506      enable: true
507      server_command: "rust-analyzer"
508"#;
509        settings.load_yaml(yaml).unwrap();
510
511        assert!(*adapters::language::lsp::enable(&settings, "rust"));
512        assert_eq!(adapters::language::lsp::server_command(&settings, "rust"), "rust-analyzer");
513    }
514
515    #[test]
516    fn test_multiple_languages_independent() {
517        let (mut settings, _dir) = create_test_settings();
518        let yaml = r#"
519languages:
520  cpp:
521    lsp:
522      enable: true
523      server_command: "clangd"
524  rust:
525    lsp:
526      enable: false
527      server_command: "rust-analyzer"
528"#;
529        settings.load_yaml(yaml).unwrap();
530
531        assert!(*adapters::language::lsp::enable(&settings, "cpp"));
532        assert_eq!(adapters::language::lsp::server_command(&settings, "cpp"), "clangd");
533        assert!(!*adapters::language::lsp::enable(&settings, "rust"));
534        assert_eq!(adapters::language::lsp::server_command(&settings, "rust"), "rust-analyzer");
535    }
536
537    #[test]
538    fn test_global_lsp_unaffected_by_language_override() {
539        let (mut settings, _dir) = create_test_settings();
540        let yaml = r#"
541languages:
542  cpp:
543    lsp:
544      enable: true
545"#;
546        settings.load_yaml(yaml).unwrap();
547
548        // Global lsp.enable should still be false (from default_settings.yaml)
549        let global_enable: &bool = settings.get("lsp.enable");
550        assert!(!*global_enable);
551    }
552}
553
554
555