tree_sitter_config/
tree_sitter_config.rs

1#![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/// Holds the contents of tree-sitter's configuration file.
54///
55/// The file typically lives at `~/.config/tree-sitter/config.json`, but see the [`Config::load`][]
56/// method for the full details on where it might be located.
57///
58/// This type holds the generic JSON content of the configuration file.  Individual tree-sitter
59/// components will use the [`Config::get`][] method to parse that JSON to extract configuration
60/// fields that are specific to that component.
61#[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() // `$HOME/Library/Application Support/`
88                .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    /// Locates and loads in the user's configuration file.  We search for the configuration file
125    /// in the following locations, in order:
126    ///
127    ///   - Location specified by the path parameter if provided
128    ///   - `$TREE_SITTER_DIR/config.json`, if the `TREE_SITTER_DIR` environment variable is set
129    ///   - `tree-sitter/config.json` in your default user configuration directory, as determined by
130    ///     [`etcetera::choose_base_strategy`](https://docs.rs/etcetera/*/etcetera/#basestrategy)
131    ///   - `$HOME/.tree-sitter/config.json` as a fallback from where tree-sitter _used_ to store
132    ///     its configuration
133    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    /// Creates an empty initial configuration file.  You can then use the [`Config::add`][] method
150    /// to add the component-specific configuration types for any components that want to add
151    /// content to the default file, and then use [`Config::save`][] to write the configuration to
152    /// disk.
153    ///
154    /// (Note that this is typically only done by the `tree-sitter init-config` command.)
155    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    /// Saves this configuration to the file that it was originally loaded from.
168    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    /// Parses a component-specific configuration from the configuration file.  The type `C` must
179    /// be [deserializable](https://docs.rs/serde/*/serde/trait.Deserialize.html) from a JSON
180    /// object, and must only include the fields relevant to that component.
181    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    /// Adds a component-specific configuration to the configuration file.  The type `C` must be
190    /// [serializable](https://docs.rs/serde/*/serde/trait.Serialize.html) into a JSON object, and
191    /// must only include the fields relevant to that component.
192    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}