1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::Path;
5use tracing::warn;
6
7#[derive(Debug, Serialize, Deserialize, Clone)]
8pub struct Config {
9 #[serde(default)]
10 pub engines: EngineConfig,
11 #[serde(default)]
12 pub rules: HashMap<String, RuleConfig>,
13 #[serde(default = "default_exclude")]
14 pub exclude: Vec<String>,
15 #[serde(default)]
16 pub auto_fix: Vec<AutoFixRule>,
17 #[serde(default)]
18 pub performance: PerformanceConfig,
19 #[serde(default)]
20 pub dictionaries: DictionaryConfig,
21 #[serde(default)]
22 pub languages: LanguageConfig,
23 #[serde(default)]
24 pub workspace: WorkspaceConfig,
25 #[serde(default)]
26 pub names: NameConfig,
27}
28
29#[derive(Debug, Serialize, Deserialize, Clone, Default)]
40pub struct NameConfig {
41 #[serde(default)]
43 pub enabled: bool,
44 #[serde(default)]
47 pub aggressiveness: crate::names::Aggressiveness,
48}
49
50#[derive(Debug, Serialize, Deserialize, Clone, Default)]
63pub struct LanguageConfig {
64 #[serde(default)]
66 pub extensions: HashMap<String, Vec<String>>,
67 #[serde(default)]
69 pub latex: LaTeXConfig,
70}
71
72#[derive(Debug, Serialize, Deserialize, Clone, Default)]
82pub struct LaTeXConfig {
83 #[serde(default)]
86 pub skip_environments: Vec<String>,
87 #[serde(default)]
91 pub skip_commands: Vec<String>,
92}
93
94#[derive(Debug, Serialize, Deserialize, Clone, Default)]
101pub struct WorkspaceConfig {
102 #[serde(default)]
105 pub index_on_open: bool,
106 #[serde(default)]
109 pub db_path: Option<String>,
110}
111
112#[derive(Debug, Serialize, Deserialize, Clone)]
115pub struct PerformanceConfig {
116 #[serde(default)]
118 pub high_performance_mode: bool,
119 #[serde(default = "default_debounce_ms")]
121 pub debounce_ms: u64,
122 #[serde(default)]
124 pub max_file_size: usize,
125}
126
127impl Default for PerformanceConfig {
128 fn default() -> Self {
129 Self {
130 high_performance_mode: false,
131 debounce_ms: 300,
132 max_file_size: 0,
133 }
134 }
135}
136
137const fn default_debounce_ms() -> u64 {
138 300
139}
140
141#[derive(Debug, Serialize, Deserialize, Clone)]
143pub struct DictionaryConfig {
144 #[serde(default = "default_true")]
147 pub bundled: bool,
148 #[serde(default)]
151 pub paths: Vec<String>,
152}
153
154impl Default for DictionaryConfig {
155 fn default() -> Self {
156 Self {
157 bundled: true,
158 paths: Vec::new(),
159 }
160 }
161}
162
163#[derive(Debug, Serialize, Deserialize, Clone)]
165pub struct AutoFixRule {
166 pub find: String,
168 pub replace: String,
170 #[serde(default)]
172 pub context: Option<String>,
173 #[serde(default)]
175 pub description: Option<String>,
176}
177
178#[derive(Debug, Serialize, Deserialize, Clone)]
179pub struct EngineConfig {
180 #[serde(
181 default = "default_harper_config",
182 deserialize_with = "deser_engine_or_bool"
183 )]
184 pub harper: HarperConfig,
185 #[serde(default, deserialize_with = "deser_engine_or_bool")]
186 pub languagetool: LanguageToolConfig,
187 #[serde(default, deserialize_with = "deser_engine_or_bool")]
188 pub vale: ValeConfig,
189 #[serde(default, deserialize_with = "deser_engine_or_bool")]
190 pub proselint: ProselintConfig,
191 #[serde(default)]
193 pub external: Vec<ExternalProvider>,
194 #[serde(default)]
196 pub wasm_plugins: Vec<WasmPlugin>,
197 #[serde(default = "default_spell_language")]
199 pub spell_language: String,
200}
201
202fn deser_engine_or_bool<'de, D, T>(deserializer: D) -> Result<T, D::Error>
205where
206 D: serde::Deserializer<'de>,
207 T: Deserialize<'de> + EngineToggle + Default,
208{
209 #[derive(Deserialize)]
210 #[serde(untagged)]
211 enum BoolOrStruct<T> {
212 Bool(bool),
213 Struct(T),
214 }
215
216 match BoolOrStruct::deserialize(deserializer)? {
217 BoolOrStruct::Bool(b) => {
218 let mut cfg = T::default();
219 cfg.set_enabled(b);
220 Ok(cfg)
221 }
222 BoolOrStruct::Struct(s) => Ok(s),
223 }
224}
225
226pub trait EngineToggle {
228 fn enabled(&self) -> bool;
229 fn set_enabled(&mut self, v: bool);
230}
231
232#[derive(Debug, Serialize, Deserialize, Clone)]
234pub struct HarperConfig {
235 #[serde(default = "default_true")]
236 pub enabled: bool,
237 #[serde(default = "default_dialect")]
239 pub dialect: String,
240 #[serde(default)]
243 pub linters: HashMap<String, bool>,
244}
245
246impl Default for HarperConfig {
247 fn default() -> Self {
248 Self {
249 enabled: true,
250 dialect: "American".to_string(),
251 linters: HashMap::new(),
252 }
253 }
254}
255
256fn default_harper_config() -> HarperConfig {
257 HarperConfig::default()
258}
259
260fn default_dialect() -> String {
261 "American".to_string()
262}
263
264impl EngineToggle for HarperConfig {
265 fn enabled(&self) -> bool {
266 self.enabled
267 }
268 fn set_enabled(&mut self, v: bool) {
269 self.enabled = v;
270 }
271}
272
273#[derive(Debug, Serialize, Deserialize, Clone)]
275pub struct LanguageToolConfig {
276 #[serde(default)]
277 pub enabled: bool,
278 #[serde(default = "default_lt_url")]
280 pub url: String,
281 #[serde(default = "default_lt_level")]
283 pub level: String,
284 #[serde(default)]
286 pub mother_tongue: Option<String>,
287 #[serde(default)]
289 pub disabled_rules: Vec<String>,
290 #[serde(default)]
292 pub enabled_rules: Vec<String>,
293 #[serde(default)]
295 pub disabled_categories: Vec<String>,
296 #[serde(default)]
298 pub enabled_categories: Vec<String>,
299}
300
301impl Default for LanguageToolConfig {
302 fn default() -> Self {
303 Self {
304 enabled: false,
305 url: default_lt_url(),
306 level: "default".to_string(),
307 mother_tongue: None,
308 disabled_rules: Vec::new(),
309 enabled_rules: Vec::new(),
310 disabled_categories: Vec::new(),
311 enabled_categories: Vec::new(),
312 }
313 }
314}
315
316fn default_lt_level() -> String {
317 "default".to_string()
318}
319
320impl EngineToggle for LanguageToolConfig {
321 fn enabled(&self) -> bool {
322 self.enabled
323 }
324 fn set_enabled(&mut self, v: bool) {
325 self.enabled = v;
326 }
327}
328
329#[derive(Debug, Default, Serialize, Deserialize, Clone)]
331pub struct ValeConfig {
332 #[serde(default)]
333 pub enabled: bool,
334 #[serde(default)]
336 pub config: Option<String>,
337}
338
339impl EngineToggle for ValeConfig {
340 fn enabled(&self) -> bool {
341 self.enabled
342 }
343 fn set_enabled(&mut self, v: bool) {
344 self.enabled = v;
345 }
346}
347
348#[derive(Debug, Default, Serialize, Deserialize, Clone)]
350pub struct ProselintConfig {
351 #[serde(default)]
352 pub enabled: bool,
353 #[serde(default)]
355 pub config: Option<String>,
356}
357
358impl EngineToggle for ProselintConfig {
359 fn enabled(&self) -> bool {
360 self.enabled
361 }
362 fn set_enabled(&mut self, v: bool) {
363 self.enabled = v;
364 }
365}
366
367#[derive(Debug, Serialize, Deserialize, Clone)]
372pub struct ExternalProvider {
373 pub name: String,
375 pub command: String,
377 #[serde(default)]
379 pub args: Vec<String>,
380 #[serde(default)]
382 pub extensions: Vec<String>,
383}
384
385#[derive(Debug, Serialize, Deserialize, Clone)]
390pub struct WasmPlugin {
391 pub name: String,
393 pub path: String,
395 #[serde(default)]
397 pub extensions: Vec<String>,
398}
399
400impl Default for EngineConfig {
401 fn default() -> Self {
402 Self {
403 harper: HarperConfig::default(),
404 languagetool: LanguageToolConfig::default(),
405 vale: ValeConfig::default(),
406 proselint: ProselintConfig::default(),
407 external: Vec::new(),
408 wasm_plugins: Vec::new(),
409 spell_language: default_spell_language(),
410 }
411 }
412}
413
414#[derive(Debug, Serialize, Deserialize, Clone)]
415pub struct RuleConfig {
416 pub severity: Option<String>, }
418
419const fn default_true() -> bool {
420 true
421}
422fn default_lt_url() -> String {
423 "http://localhost:8010".to_string()
424}
425fn default_spell_language() -> String {
426 "en-US".to_string()
427}
428fn default_exclude() -> Vec<String> {
429 vec![
430 "node_modules/**".to_string(),
431 ".git/**".to_string(),
432 "target/**".to_string(),
433 "dist/**".to_string(),
434 "build/**".to_string(),
435 ".next/**".to_string(),
436 ".nuxt/**".to_string(),
437 "vendor/**".to_string(),
438 "__pycache__/**".to_string(),
439 ".venv/**".to_string(),
440 "venv/**".to_string(),
441 ".tox/**".to_string(),
442 ".mypy_cache/**".to_string(),
443 "*.min.js".to_string(),
444 "*.min.css".to_string(),
445 "*.bundle.js".to_string(),
446 "package-lock.json".to_string(),
447 "yarn.lock".to_string(),
448 "pnpm-lock.yaml".to_string(),
449 ]
450}
451
452impl Config {
453 pub fn load(workspace_root: &Path) -> Result<Self> {
454 let yaml_path = workspace_root.join(".languagecheck.yaml");
456 let yml_path = workspace_root.join(".languagecheck.yml");
457 let json_path = workspace_root.join(".languagecheck.json");
458
459 if yaml_path.exists() {
460 let content = std::fs::read_to_string(yaml_path)?;
461 warn_duplicate_rule_keys(&content);
462 let config: Self = serde_yaml::from_str(&content)?;
463 Ok(config)
464 } else if yml_path.exists() {
465 let content = std::fs::read_to_string(yml_path)?;
466 warn_duplicate_rule_keys(&content);
467 let config: Self = serde_yaml::from_str(&content)?;
468 Ok(config)
469 } else if json_path.exists() {
470 let content = std::fs::read_to_string(json_path)?;
471 let config: Self = serde_json::from_str(&content)?;
472 Ok(config)
473 } else {
474 Ok(Self::default())
475 }
476 }
477
478 #[must_use]
481 pub fn apply_auto_fixes(&self, text: &str) -> (String, usize) {
482 let mut result = text.to_string();
483 let mut total = 0;
484
485 for rule in &self.auto_fix {
486 if let Some(ctx) = &rule.context
487 && !result.contains(ctx.as_str())
488 {
489 continue;
490 }
491 let count = result.matches(&rule.find).count();
492 if count > 0 {
493 result = result.replace(&rule.find, &rule.replace);
494 total += count;
495 }
496 }
497
498 (result, total)
499 }
500}
501
502fn duplicate_rule_keys(content: &str) -> Vec<String> {
510 let mut in_rules = false;
511 let mut child_indent: Option<usize> = None;
512 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
513 let mut duplicates: Vec<String> = Vec::new();
514
515 for line in content.lines() {
516 if line.trim().is_empty() {
517 continue;
518 }
519 let indent = line.len() - line.trim_start().len();
520
521 if !in_rules {
522 if indent == 0 && line.trim() == "rules:" {
523 in_rules = true;
524 }
525 continue;
526 }
527
528 if indent == 0 {
530 break;
531 }
532
533 let child = *child_indent.get_or_insert(indent);
534 if indent != child {
535 continue; }
537 if let Some(key) = line.trim().strip_suffix(':') {
538 let key = key.trim().to_string();
539 if !key.is_empty() && !seen.insert(key.clone()) && !duplicates.contains(&key) {
540 duplicates.push(key);
541 }
542 }
543 }
544
545 duplicates
546}
547
548fn warn_duplicate_rule_keys(content: &str) {
550 let duplicates = duplicate_rule_keys(content);
551 if !duplicates.is_empty() {
552 warn!(
553 duplicates = ?duplicates,
554 "Duplicate rule keys in .languagecheck.yaml; only the last entry for each takes \
555 effect. Remove the extra copies to keep the ignore list clean."
556 );
557 }
558}
559
560impl Default for Config {
561 fn default() -> Self {
562 Self {
563 engines: EngineConfig::default(),
564 rules: HashMap::new(),
565 exclude: default_exclude(),
566 auto_fix: Vec::new(),
567 performance: PerformanceConfig::default(),
568 dictionaries: DictionaryConfig::default(),
569 languages: LanguageConfig::default(),
570 workspace: WorkspaceConfig::default(),
571 names: NameConfig::default(),
572 }
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579
580 #[test]
581 fn duplicate_rule_keys_detects_repeats() {
582 let yaml = "rules:\n languagetool.ARROWS:\n severity: \"off\"\n \
583 languagetool.UPPERCASE_SENTENCE_START:\n severity: \"off\"\n \
584 languagetool.ARROWS:\n severity: \"off\"\n \
585 languagetool.UPPERCASE_SENTENCE_START:\n severity: \"off\"\n \
586 languagetool.THE_SUPERLATIVE:\n severity: \"off\"\n";
587 let dups = duplicate_rule_keys(yaml);
588 assert_eq!(
589 dups,
590 vec![
591 "languagetool.ARROWS".to_string(),
592 "languagetool.UPPERCASE_SENTENCE_START".to_string()
593 ]
594 );
595 }
596
597 #[test]
598 fn duplicate_rule_keys_clean_list_is_empty() {
599 let yaml = "rules:\n a.B:\n severity: \"off\"\n c.D:\n severity: \"off\"\n";
600 assert!(duplicate_rule_keys(yaml).is_empty());
601 }
602
603 #[test]
604 fn duplicate_rule_keys_stops_at_next_section() {
605 let yaml = "rules:\n a.B:\n severity: \"off\"\nengines:\n harper: false\n";
607 assert!(duplicate_rule_keys(yaml).is_empty());
608 }
609
610 #[test]
611 fn default_config_has_harper_enabled_lt_disabled() {
612 let config = Config::default();
613 assert!(config.engines.harper.enabled);
614 assert!(!config.engines.languagetool.enabled);
615 }
616
617 #[test]
618 fn default_config_has_standard_excludes() {
619 let config = Config::default();
620 assert!(config.exclude.contains(&"node_modules/**".to_string()));
621 assert!(config.exclude.contains(&".git/**".to_string()));
622 assert!(config.exclude.contains(&"target/**".to_string()));
623 assert!(config.exclude.contains(&"dist/**".to_string()));
624 assert!(config.exclude.contains(&"vendor/**".to_string()));
625 }
626
627 #[test]
628 fn default_lt_url() {
629 let config = Config::default();
630 assert_eq!(config.engines.languagetool.url, "http://localhost:8010");
631 }
632
633 #[test]
634 fn load_from_json_string() {
635 let json = r#"{
636 "engines": { "harper": true, "languagetool": false },
637 "rules": { "spelling.typo": { "severity": "warning" } }
638 }"#;
639 let config: Config = serde_json::from_str(json).unwrap();
640 assert!(config.engines.harper.enabled);
641 assert!(!config.engines.languagetool.enabled);
642 assert!(config.rules.contains_key("spelling.typo"));
643 assert_eq!(
644 config.rules["spelling.typo"].severity.as_deref(),
645 Some("warning")
646 );
647 }
648
649 #[test]
650 fn load_partial_json_uses_defaults() {
651 let json = r#"{}"#;
652 let config: Config = serde_json::from_str(json).unwrap();
653 assert!(config.engines.harper.enabled);
654 assert!(!config.engines.languagetool.enabled);
655 assert!(config.rules.is_empty());
656 }
657
658 #[test]
659 fn load_from_json_file() {
660 let dir = std::env::temp_dir().join("lang_check_test_config_json");
661 let _ = std::fs::remove_dir_all(&dir);
662 std::fs::create_dir_all(&dir).unwrap();
663
664 let config_path = dir.join(".languagecheck.json");
665 std::fs::write(
666 &config_path,
667 r#"{"engines": {"harper": false, "languagetool": true}}"#,
668 )
669 .unwrap();
670
671 let config = Config::load(&dir).unwrap();
672 assert!(!config.engines.harper.enabled);
673 assert!(config.engines.languagetool.enabled);
674
675 let _ = std::fs::remove_dir_all(&dir);
676 }
677
678 #[test]
679 fn load_from_yaml_file() {
680 let dir = std::env::temp_dir().join("lang_check_test_config_yaml");
681 let _ = std::fs::remove_dir_all(&dir);
682 std::fs::create_dir_all(&dir).unwrap();
683
684 let config_path = dir.join(".languagecheck.yaml");
685 std::fs::write(
686 &config_path,
687 "engines:\n harper: false\n languagetool: true\n",
688 )
689 .unwrap();
690
691 let config = Config::load(&dir).unwrap();
692 assert!(!config.engines.harper.enabled);
693 assert!(config.engines.languagetool.enabled);
694
695 let _ = std::fs::remove_dir_all(&dir);
696 }
697
698 #[test]
699 fn yaml_takes_precedence_over_json() {
700 let dir = std::env::temp_dir().join("lang_check_test_config_precedence");
701 let _ = std::fs::remove_dir_all(&dir);
702 std::fs::create_dir_all(&dir).unwrap();
703
704 std::fs::write(
706 dir.join(".languagecheck.yaml"),
707 "engines:\n harper: false\n",
708 )
709 .unwrap();
710 std::fs::write(
711 dir.join(".languagecheck.json"),
712 r#"{"engines": {"harper": true}}"#,
713 )
714 .unwrap();
715
716 let config = Config::load(&dir).unwrap();
717 assert!(!config.engines.harper.enabled);
719
720 let _ = std::fs::remove_dir_all(&dir);
721 }
722
723 #[test]
724 fn load_missing_file_returns_default() {
725 let dir = std::env::temp_dir().join("lang_check_test_config_missing");
726 let _ = std::fs::remove_dir_all(&dir);
727 std::fs::create_dir_all(&dir).unwrap();
728
729 let config = Config::load(&dir).unwrap();
730 assert!(config.engines.harper.enabled);
731
732 let _ = std::fs::remove_dir_all(&dir);
733 }
734
735 #[test]
736 fn auto_fix_simple_replacement() {
737 let config = Config {
738 auto_fix: vec![AutoFixRule {
739 find: "teh".to_string(),
740 replace: "the".to_string(),
741 context: None,
742 description: None,
743 }],
744 ..Config::default()
745 };
746 let (result, count) = config.apply_auto_fixes("Fix teh typo in teh text.");
747 assert_eq!(result, "Fix the typo in the text.");
748 assert_eq!(count, 2);
749 }
750
751 #[test]
752 fn auto_fix_with_context_filter() {
753 let config = Config {
754 auto_fix: vec![AutoFixRule {
755 find: "colour".to_string(),
756 replace: "color".to_string(),
757 context: Some("American".to_string()),
758 description: Some("Use American spelling".to_string()),
759 }],
760 ..Config::default()
761 };
762 let (result, count) = config.apply_auto_fixes("American English: the colour is red.");
764 assert_eq!(result, "American English: the color is red.");
765 assert_eq!(count, 1);
766
767 let (result, count) = config.apply_auto_fixes("British English: the colour is red.");
769 assert_eq!(result, "British English: the colour is red.");
770 assert_eq!(count, 0);
771 }
772
773 #[test]
774 fn auto_fix_no_match() {
775 let config = Config {
776 auto_fix: vec![AutoFixRule {
777 find: "foo".to_string(),
778 replace: "bar".to_string(),
779 context: None,
780 description: None,
781 }],
782 ..Config::default()
783 };
784 let (result, count) = config.apply_auto_fixes("No matches here.");
785 assert_eq!(result, "No matches here.");
786 assert_eq!(count, 0);
787 }
788
789 #[test]
790 fn auto_fix_multiple_rules() {
791 let config = Config {
792 auto_fix: vec![
793 AutoFixRule {
794 find: "recieve".to_string(),
795 replace: "receive".to_string(),
796 context: None,
797 description: None,
798 },
799 AutoFixRule {
800 find: "seperate".to_string(),
801 replace: "separate".to_string(),
802 context: None,
803 description: None,
804 },
805 ],
806 ..Config::default()
807 };
808 let (result, count) = config.apply_auto_fixes("Please recieve the seperate package.");
809 assert_eq!(result, "Please receive the separate package.");
810 assert_eq!(count, 2);
811 }
812
813 #[test]
814 fn auto_fix_loads_from_yaml() {
815 let yaml = r#"
816auto_fix:
817 - find: "teh"
818 replace: "the"
819 description: "Fix common typo"
820 - find: "colour"
821 replace: "color"
822 context: "American"
823"#;
824 let config: Config = serde_yaml::from_str(yaml).unwrap();
825 assert_eq!(config.auto_fix.len(), 2);
826 assert_eq!(config.auto_fix[0].find, "teh");
827 assert_eq!(config.auto_fix[0].replace, "the");
828 assert_eq!(
829 config.auto_fix[0].description.as_deref(),
830 Some("Fix common typo")
831 );
832 assert_eq!(config.auto_fix[1].context.as_deref(), Some("American"));
833 }
834
835 #[test]
836 fn default_config_has_empty_auto_fix() {
837 let config = Config::default();
838 assert!(config.auto_fix.is_empty());
839 }
840
841 #[test]
842 fn external_providers_from_yaml() {
843 let yaml = r#"
844engines:
845 harper: true
846 languagetool: false
847 external:
848 - name: vale
849 command: /usr/bin/vale
850 args: ["--output", "JSON"]
851 extensions: [md, rst]
852 - name: custom-checker
853 command: ./my-checker
854"#;
855 let config: Config = serde_yaml::from_str(yaml).unwrap();
856 assert_eq!(config.engines.external.len(), 2);
857 assert_eq!(config.engines.external[0].name, "vale");
858 assert_eq!(config.engines.external[0].command, "/usr/bin/vale");
859 assert_eq!(config.engines.external[0].args, vec!["--output", "JSON"]);
860 assert_eq!(config.engines.external[0].extensions, vec!["md", "rst"]);
861 assert_eq!(config.engines.external[1].name, "custom-checker");
862 assert!(config.engines.external[1].args.is_empty());
863 }
864
865 #[test]
866 fn default_config_has_no_external_providers() {
867 let config = Config::default();
868 assert!(config.engines.external.is_empty());
869 }
870
871 #[test]
872 fn wasm_plugins_from_yaml() {
873 let yaml = r#"
874engines:
875 harper: true
876 wasm_plugins:
877 - name: custom-checker
878 path: .languagecheck/plugins/checker.wasm
879 extensions: [md, html]
880 - name: style-linter
881 path: /opt/plugins/style.wasm
882"#;
883 let config: Config = serde_yaml::from_str(yaml).unwrap();
884 assert_eq!(config.engines.wasm_plugins.len(), 2);
885 assert_eq!(config.engines.wasm_plugins[0].name, "custom-checker");
886 assert_eq!(
887 config.engines.wasm_plugins[0].path,
888 ".languagecheck/plugins/checker.wasm"
889 );
890 assert_eq!(
891 config.engines.wasm_plugins[0].extensions,
892 vec!["md", "html"]
893 );
894 assert_eq!(config.engines.wasm_plugins[1].name, "style-linter");
895 assert!(config.engines.wasm_plugins[1].extensions.is_empty());
896 }
897
898 #[test]
899 fn default_config_has_no_wasm_plugins() {
900 let config = Config::default();
901 assert!(config.engines.wasm_plugins.is_empty());
902 }
903
904 #[test]
905 fn performance_config_defaults() {
906 let config = Config::default();
907 assert!(!config.performance.high_performance_mode);
908 assert_eq!(config.performance.debounce_ms, 300);
909 assert_eq!(config.performance.max_file_size, 0);
910 }
911
912 #[test]
913 fn performance_config_from_yaml() {
914 let yaml = r#"
915performance:
916 high_performance_mode: true
917 debounce_ms: 500
918 max_file_size: 1048576
919"#;
920 let config: Config = serde_yaml::from_str(yaml).unwrap();
921 assert!(config.performance.high_performance_mode);
922 assert_eq!(config.performance.debounce_ms, 500);
923 assert_eq!(config.performance.max_file_size, 1_048_576);
924 }
925
926 #[test]
927 fn latex_skip_environments_from_yaml() {
928 let yaml = r#"
929languages:
930 latex:
931 skip_environments:
932 - prooftree
933 - mycustomenv
934"#;
935 let config: Config = serde_yaml::from_str(yaml).unwrap();
936 assert_eq!(
937 config.languages.latex.skip_environments,
938 vec!["prooftree", "mycustomenv"]
939 );
940 }
941
942 #[test]
943 fn default_config_has_empty_latex_skip_environments() {
944 let config = Config::default();
945 assert!(config.languages.latex.skip_environments.is_empty());
946 }
947
948 #[test]
949 fn latex_skip_commands_from_yaml() {
950 let yaml = r#"
951languages:
952 latex:
953 skip_commands:
954 - codefont
955 - myverb
956"#;
957 let config: Config = serde_yaml::from_str(yaml).unwrap();
958 assert_eq!(
959 config.languages.latex.skip_commands,
960 vec!["codefont", "myverb"]
961 );
962 }
963
964 #[test]
965 fn default_spell_language_is_en_us() {
966 let config = Config::default();
967 assert_eq!(config.engines.spell_language, "en-US");
968 }
969
970 #[test]
971 fn spell_language_from_yaml() {
972 let yaml = r#"
973engines:
974 spell_language: de-DE
975"#;
976 let config: Config = serde_yaml::from_str(yaml).unwrap();
977 assert_eq!(config.engines.spell_language, "de-DE");
978 }
979
980 #[test]
981 fn default_config_has_empty_latex_skip_commands() {
982 let config = Config::default();
983 assert!(config.languages.latex.skip_commands.is_empty());
984 }
985
986 #[test]
987 fn default_vale_is_disabled() {
988 let config = Config::default();
989 assert!(!config.engines.vale.enabled);
990 assert!(config.engines.vale.config.is_none());
991 }
992
993 #[test]
994 fn vale_bool_shorthand_from_yaml() {
995 let yaml = r#"
996engines:
997 vale: true
998"#;
999 let config: Config = serde_yaml::from_str(yaml).unwrap();
1000 assert!(config.engines.vale.enabled);
1001 }
1002
1003 #[test]
1004 fn vale_nested_config_from_yaml() {
1005 let yaml = r#"
1006engines:
1007 vale:
1008 enabled: true
1009 config: ".vale.ini"
1010"#;
1011 let config: Config = serde_yaml::from_str(yaml).unwrap();
1012 assert!(config.engines.vale.enabled);
1013 assert_eq!(config.engines.vale.config.as_deref(), Some(".vale.ini"));
1014 }
1015
1016 #[test]
1017 fn harper_nested_config_from_yaml() {
1018 let yaml = r#"
1019engines:
1020 harper:
1021 enabled: true
1022 dialect: "British"
1023 linters:
1024 LongSentences: false
1025"#;
1026 let config: Config = serde_yaml::from_str(yaml).unwrap();
1027 assert!(config.engines.harper.enabled);
1028 assert_eq!(config.engines.harper.dialect, "British");
1029 assert_eq!(
1030 config.engines.harper.linters.get("LongSentences"),
1031 Some(&false)
1032 );
1033 }
1034
1035 #[test]
1036 fn languagetool_nested_config_from_yaml() {
1037 let yaml = r#"
1038engines:
1039 languagetool:
1040 enabled: true
1041 url: "http://localhost:9090"
1042 level: "picky"
1043 disabled_rules:
1044 - WHITESPACE_RULE
1045"#;
1046 let config: Config = serde_yaml::from_str(yaml).unwrap();
1047 assert!(config.engines.languagetool.enabled);
1048 assert_eq!(config.engines.languagetool.url, "http://localhost:9090");
1049 assert_eq!(config.engines.languagetool.level, "picky");
1050 assert_eq!(
1051 config.engines.languagetool.disabled_rules,
1052 vec!["WHITESPACE_RULE"]
1053 );
1054 }
1055
1056 #[test]
1057 fn default_proselint_is_disabled() {
1058 let config = Config::default();
1059 assert!(!config.engines.proselint.enabled);
1060 assert!(config.engines.proselint.config.is_none());
1061 }
1062
1063 #[test]
1064 fn proselint_bool_shorthand_from_yaml() {
1065 let yaml = r#"
1066engines:
1067 proselint: true
1068"#;
1069 let config: Config = serde_yaml::from_str(yaml).unwrap();
1070 assert!(config.engines.proselint.enabled);
1071 }
1072
1073 #[test]
1074 fn proselint_nested_config_from_yaml() {
1075 let yaml = r#"
1076engines:
1077 proselint:
1078 enabled: true
1079 config: "proselint.json"
1080"#;
1081 let config: Config = serde_yaml::from_str(yaml).unwrap();
1082 assert!(config.engines.proselint.enabled);
1083 assert_eq!(
1084 config.engines.proselint.config.as_deref(),
1085 Some("proselint.json")
1086 );
1087 }
1088}