use crate::input_processing::TreeSitterProcessor;
use crate::{parse::GrammarConfig, render::RenderConfig};
use anyhow::{Context, Result};
use json5 as json;
use log::info;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs, io,
path::{Path, PathBuf},
};
use thiserror::Error;
#[cfg(target_os = "windows")]
use directories_next::ProjectDirs;
const CFG_FILE_NAME: &str = "config.json5";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "kebab-case", default)]
pub struct Config {
pub file_associations: Option<HashMap<String, String>>,
pub formatting: RenderConfig,
pub grammar: GrammarConfig,
pub input_processing: TreeSitterProcessor,
pub fallback_cmd: Option<String>,
}
#[derive(Error, Debug)]
pub enum ReadError {
#[error("The file failed to deserialize")]
DeserializationFailure(#[from] anyhow::Error),
#[error("Failed to read the config file")]
ReadFileFailure(#[from] io::Error),
#[error("Unable to compute the default config file path")]
NoDefault,
}
impl Config {
pub fn try_from_file<P: AsRef<Path>>(path: Option<&P>) -> Result<Self, ReadError> {
#[allow(unused_assignments)]
let mut default_config_fp = PathBuf::new();
let config_fp = if let Some(p) = path {
p.as_ref()
} else {
default_config_fp = default_config_file_path().map_err(|_| ReadError::NoDefault)?;
default_config_fp.as_ref()
};
info!("Reading config at {}", config_fp.to_string_lossy());
let config_contents = fs::read_to_string(config_fp)?;
let config = json::from_str(&config_contents)
.with_context(|| format!("Failed to parse config at {}", config_fp.to_string_lossy()))
.map_err(ReadError::DeserializationFailure)?;
Ok(config)
}
}
#[cfg(not(target_os = "windows"))]
fn default_config_file_path() -> Result<PathBuf> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("diffsitter")?;
let file_path = xdg_dirs.place_config_file(CFG_FILE_NAME)?;
Ok(file_path)
}
#[cfg(target_os = "windows")]
fn default_config_file_path() -> Result<PathBuf> {
use anyhow::ensure;
let proj_dirs = ProjectDirs::from("io", "afnan", "diffsitter");
ensure!(proj_dirs.is_some(), "Was not able to retrieve config path");
let proj_dirs = proj_dirs.unwrap();
let mut config_file: PathBuf = proj_dirs.config_dir().into();
config_file.push(CFG_FILE_NAME);
Ok(config_file)
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Context;
use std::{env, fs::read_dir};
#[test]
fn test_sample_config() {
let repo_root =
env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env::var("BUILD_DIR").unwrap());
assert!(!repo_root.is_empty());
let sample_config_path = [repo_root, "assets".into(), "sample_config.json5".into()]
.iter()
.collect::<PathBuf>();
assert!(sample_config_path.exists());
Config::try_from_file(Some(sample_config_path).as_ref()).unwrap();
}
#[test]
fn test_configs() {
let mut test_config_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
test_config_dir.push("resources/test_configs");
assert!(test_config_dir.is_dir());
for config_file_path in read_dir(test_config_dir).unwrap() {
let config_file_path = config_file_path.unwrap().path();
let has_correct_ext = if let Some(ext) = config_file_path.extension() {
ext == "json5"
} else {
false
};
if !config_file_path.is_file() || !has_correct_ext {
continue;
}
Config::try_from_file(Some(&config_file_path))
.with_context(|| {
format!(
"Parsing file {}",
&config_file_path.file_name().unwrap().to_string_lossy()
)
})
.unwrap();
}
}
}