1use crate::input_processing::TreeSitterProcessor;
4use crate::{parse::GrammarConfig, render::RenderConfig};
5use anyhow::{Context, Result};
6use json5 as json;
7use log::info;
8use serde::{Deserialize, Serialize};
9use std::{
10 collections::HashMap,
11 fs, io,
12 path::{Path, PathBuf},
13};
14use thiserror::Error;
15
16#[cfg(target_os = "windows")]
17use directories_next::ProjectDirs;
18
19const CFG_FILE_NAME: &str = "config.json5";
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
24#[serde(rename_all = "kebab-case", default)]
25pub struct Config {
26 pub file_associations: Option<HashMap<String, String>>,
32
33 pub formatting: RenderConfig,
35
36 pub grammar: GrammarConfig,
38
39 pub input_processing: TreeSitterProcessor,
41
42 pub fallback_cmd: Option<String>,
51}
52
53#[derive(Error, Debug)]
55pub enum ReadError {
56 #[error("The file failed to deserialize")]
57 DeserializationFailure(#[from] anyhow::Error),
58 #[error("Failed to read the config file")]
59 ReadFileFailure(#[from] io::Error),
60 #[error("Unable to compute the default config file path")]
61 NoDefault,
62}
63
64impl Config {
65 pub fn try_from_file<P: AsRef<Path>>(path: Option<&P>) -> Result<Self, ReadError> {
77 #[allow(unused_assignments)]
81 let mut default_config_fp = PathBuf::new();
82
83 let config_fp = if let Some(p) = path {
84 p.as_ref()
85 } else {
86 default_config_fp = default_config_file_path().map_err(|_| ReadError::NoDefault)?;
87 default_config_fp.as_ref()
88 };
89 info!("Reading config at {}", config_fp.to_string_lossy());
90 let config_contents = fs::read_to_string(config_fp)?;
91 let config = json::from_str(&config_contents)
92 .with_context(|| format!("Failed to parse config at {}", config_fp.to_string_lossy()))
93 .map_err(ReadError::DeserializationFailure)?;
94 Ok(config)
95 }
96}
97
98#[cfg(not(target_os = "windows"))]
101fn default_config_file_path() -> Result<PathBuf> {
102 let xdg_dirs = xdg::BaseDirectories::with_prefix("diffsitter")?;
103 let file_path = xdg_dirs.place_config_file(CFG_FILE_NAME)?;
104 Ok(file_path)
105}
106
107#[cfg(target_os = "windows")]
110fn default_config_file_path() -> Result<PathBuf> {
111 use anyhow::ensure;
112
113 let proj_dirs = ProjectDirs::from("io", "afnan", "diffsitter");
114 ensure!(proj_dirs.is_some(), "Was not able to retrieve config path");
115 let proj_dirs = proj_dirs.unwrap();
116 let mut config_file: PathBuf = proj_dirs.config_dir().into();
117 config_file.push(CFG_FILE_NAME);
118 Ok(config_file)
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use anyhow::Context;
125 use std::{env, fs::read_dir};
126
127 #[test]
128 fn test_sample_config() {
129 let repo_root =
130 env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env::var("BUILD_DIR").unwrap());
131 assert!(!repo_root.is_empty());
132 let sample_config_path = [repo_root, "assets".into(), "sample_config.json5".into()]
133 .iter()
134 .collect::<PathBuf>();
135 assert!(sample_config_path.exists());
136 Config::try_from_file(Some(sample_config_path).as_ref()).unwrap();
137 }
138
139 #[test]
140 fn test_configs() {
141 let mut test_config_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
142 test_config_dir.push("resources/test_configs");
143 assert!(test_config_dir.is_dir());
144
145 for config_file_path in read_dir(test_config_dir).unwrap() {
146 let config_file_path = config_file_path.unwrap().path();
147 let has_correct_ext = if let Some(ext) = config_file_path.extension() {
148 ext == "json5"
149 } else {
150 false
151 };
152 if !config_file_path.is_file() || !has_correct_ext {
153 continue;
154 }
155 Config::try_from_file(Some(&config_file_path))
159 .with_context(|| {
160 format!(
161 "Parsing file {}",
162 &config_file_path.file_name().unwrap().to_string_lossy()
163 )
164 })
165 .unwrap();
166 }
167 }
168}