tree_sitter_config/
tree_sitter_config.rs1#![cfg_attr(not(any(test, doctest)), doc = include_str!("../README.md"))]
2
3use std::{
4 env, fs,
5 path::{Path, PathBuf},
6};
7
8use etcetera::BaseStrategy as _;
9use log::warn;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use thiserror::Error;
13
14pub type ConfigResult<T> = Result<T, ConfigError>;
15
16#[derive(Debug, Error)]
17pub enum ConfigError {
18 #[error("Bad JSON config {0} -- {1}")]
19 ConfigRead(String, serde_json::Error),
20 #[error(transparent)]
21 HomeDir(#[from] etcetera::HomeDirError),
22 #[error(transparent)]
23 IO(IoError),
24 #[error(transparent)]
25 Serialization(#[from] serde_json::Error),
26}
27
28#[derive(Debug, Error)]
29pub struct IoError {
30 pub error: std::io::Error,
31 pub path: Option<String>,
32}
33
34impl IoError {
35 fn new(error: std::io::Error, path: Option<&Path>) -> Self {
36 Self {
37 error,
38 path: path.map(|p| p.to_string_lossy().to_string()),
39 }
40 }
41}
42
43impl std::fmt::Display for IoError {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{}", self.error)?;
46 if let Some(ref path) = self.path {
47 write!(f, " ({path})")?;
48 }
49 Ok(())
50 }
51}
52
53#[derive(Debug)]
62pub struct Config {
63 pub location: PathBuf,
64 pub config: Value,
65}
66
67impl Config {
68 pub fn find_config_file() -> ConfigResult<Option<PathBuf>> {
69 if let Ok(path) = env::var("TREE_SITTER_DIR") {
70 let mut path = PathBuf::from(path);
71 path.push("config.json");
72 if !path.exists() {
73 return Ok(None);
74 }
75 if path.is_file() {
76 return Ok(Some(path));
77 }
78 }
79
80 let xdg_path = Self::xdg_config_file()?;
81 if xdg_path.is_file() {
82 return Ok(Some(xdg_path));
83 }
84
85 if cfg!(target_os = "macos") {
86 let legacy_apple_path = etcetera::base_strategy::Apple::new()?
87 .data_dir() .join("tree-sitter")
89 .join("config.json");
90 if legacy_apple_path.is_file() {
91 let xdg_dir = xdg_path.parent().unwrap();
92 fs::create_dir_all(xdg_dir)
93 .map_err(|e| ConfigError::IO(IoError::new(e, Some(xdg_dir))))?;
94 fs::rename(&legacy_apple_path, &xdg_path).map_err(|e| {
95 ConfigError::IO(IoError::new(e, Some(legacy_apple_path.as_path())))
96 })?;
97 warn!(
98 "Your config.json file has been automatically migrated from \"{}\" to \"{}\"",
99 legacy_apple_path.display(),
100 xdg_path.display()
101 );
102 return Ok(Some(xdg_path));
103 }
104 }
105
106 let legacy_path = etcetera::home_dir()?
107 .join(".tree-sitter")
108 .join("config.json");
109 if legacy_path.is_file() {
110 return Ok(Some(legacy_path));
111 }
112
113 Ok(None)
114 }
115
116 fn xdg_config_file() -> ConfigResult<PathBuf> {
117 let xdg_path = etcetera::choose_base_strategy()?
118 .config_dir()
119 .join("tree-sitter")
120 .join("config.json");
121 Ok(xdg_path)
122 }
123
124 pub fn load(path: Option<PathBuf>) -> ConfigResult<Self> {
134 let location = if let Some(path) = path {
135 path
136 } else if let Some(path) = Self::find_config_file()? {
137 path
138 } else {
139 return Self::initial();
140 };
141
142 let content = fs::read_to_string(&location)
143 .map_err(|e| ConfigError::IO(IoError::new(e, Some(location.as_path()))))?;
144 let config = serde_json::from_str(&content)
145 .map_err(|e| ConfigError::ConfigRead(location.to_string_lossy().to_string(), e))?;
146 Ok(Self { location, config })
147 }
148
149 pub fn initial() -> ConfigResult<Self> {
156 let location = if let Ok(path) = env::var("TREE_SITTER_DIR") {
157 let mut path = PathBuf::from(path);
158 path.push("config.json");
159 path
160 } else {
161 Self::xdg_config_file()?
162 };
163 let config = serde_json::json!({});
164 Ok(Self { location, config })
165 }
166
167 pub fn save(&self) -> ConfigResult<()> {
169 let json = serde_json::to_string_pretty(&self.config)?;
170 let config_dir = self.location.parent().unwrap();
171 fs::create_dir_all(config_dir)
172 .map_err(|e| ConfigError::IO(IoError::new(e, Some(config_dir))))?;
173 fs::write(&self.location, json)
174 .map_err(|e| ConfigError::IO(IoError::new(e, Some(self.location.as_path()))))?;
175 Ok(())
176 }
177
178 pub fn get<C>(&self) -> ConfigResult<C>
182 where
183 C: for<'de> Deserialize<'de>,
184 {
185 let config = serde_json::from_value(self.config.clone())?;
186 Ok(config)
187 }
188
189 pub fn add<C>(&mut self, config: C) -> ConfigResult<()>
193 where
194 C: Serialize,
195 {
196 let mut config = serde_json::to_value(&config)?;
197 self.config
198 .as_object_mut()
199 .unwrap()
200 .append(config.as_object_mut().unwrap());
201 Ok(())
202 }
203}