tree_sitter_config/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{env, fs, path::PathBuf};
4
5use anyhow::{Context, Result};
6use etcetera::BaseStrategy as _;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// Holds the contents of tree-sitter's configuration file.
11///
12/// The file typically lives at `~/.config/tree-sitter/config.json`, but see the [`Config::load`][]
13/// method for the full details on where it might be located.
14///
15/// This type holds the generic JSON content of the configuration file.  Individual tree-sitter
16/// components will use the [`Config::get`][] method to parse that JSON to extract configuration
17/// fields that are specific to that component.
18#[derive(Debug)]
19pub struct Config {
20    pub location: PathBuf,
21    pub config: Value,
22}
23
24impl Config {
25    pub fn find_config_file() -> Result<Option<PathBuf>> {
26        if let Ok(path) = env::var("TREE_SITTER_DIR") {
27            let mut path = PathBuf::from(path);
28            path.push("config.json");
29            if !path.exists() {
30                return Ok(None);
31            }
32            if path.is_file() {
33                return Ok(Some(path));
34            }
35        }
36
37        let xdg_path = Self::xdg_config_file()?;
38        if xdg_path.is_file() {
39            return Ok(Some(xdg_path));
40        }
41
42        if cfg!(target_os = "macos") {
43            let legacy_apple_path = etcetera::base_strategy::Apple::new()?
44                .data_dir() // `$HOME/Library/Application Support/`
45                .join("tree-sitter")
46                .join("config.json");
47            if legacy_apple_path.is_file() {
48                fs::create_dir_all(xdg_path.parent().unwrap())?;
49                fs::rename(&legacy_apple_path, &xdg_path)?;
50                println!(
51                    "Warning: your config.json file has been automatically migrated from \"{}\" to \"{}\"",
52                    legacy_apple_path.display(),
53                    xdg_path.display()
54                );
55                return Ok(Some(xdg_path));
56            }
57        }
58
59        let legacy_path = etcetera::home_dir()?
60            .join(".tree-sitter")
61            .join("config.json");
62        if legacy_path.is_file() {
63            return Ok(Some(legacy_path));
64        }
65
66        Ok(None)
67    }
68
69    fn xdg_config_file() -> Result<PathBuf> {
70        let xdg_path = etcetera::choose_base_strategy()?
71            .config_dir()
72            .join("tree-sitter")
73            .join("config.json");
74        Ok(xdg_path)
75    }
76
77    /// Locates and loads in the user's configuration file.  We search for the configuration file
78    /// in the following locations, in order:
79    ///
80    ///   - Location specified by the path parameter if provided
81    ///   - `$TREE_SITTER_DIR/config.json`, if the `TREE_SITTER_DIR` environment variable is set
82    ///   - `tree-sitter/config.json` in your default user configuration directory, as determined by
83    ///     [`etcetera::choose_base_strategy`](https://docs.rs/etcetera/*/etcetera/#basestrategy)
84    ///   - `$HOME/.tree-sitter/config.json` as a fallback from where tree-sitter _used_ to store
85    ///     its configuration
86    pub fn load(path: Option<PathBuf>) -> Result<Self> {
87        let location = if let Some(path) = path {
88            path
89        } else if let Some(path) = Self::find_config_file()? {
90            path
91        } else {
92            return Self::initial();
93        };
94
95        let content = fs::read_to_string(&location)
96            .with_context(|| format!("Failed to read {}", location.to_string_lossy()))?;
97        let config = serde_json::from_str(&content)
98            .with_context(|| format!("Bad JSON config {}", location.to_string_lossy()))?;
99        Ok(Self { location, config })
100    }
101
102    /// Creates an empty initial configuration file.  You can then use the [`Config::add`][] method
103    /// to add the component-specific configuration types for any components that want to add
104    /// content to the default file, and then use [`Config::save`][] to write the configuration to
105    /// disk.
106    ///
107    /// (Note that this is typically only done by the `tree-sitter init-config` command.)
108    pub fn initial() -> Result<Self> {
109        let location = if let Ok(path) = env::var("TREE_SITTER_DIR") {
110            let mut path = PathBuf::from(path);
111            path.push("config.json");
112            path
113        } else {
114            Self::xdg_config_file()?
115        };
116        let config = serde_json::json!({});
117        Ok(Self { location, config })
118    }
119
120    /// Saves this configuration to the file that it was originally loaded from.
121    pub fn save(&self) -> Result<()> {
122        let json = serde_json::to_string_pretty(&self.config)?;
123        fs::create_dir_all(self.location.parent().unwrap())?;
124        fs::write(&self.location, json)?;
125        Ok(())
126    }
127
128    /// Parses a component-specific configuration from the configuration file.  The type `C` must
129    /// be [deserializable](https://docs.rs/serde/*/serde/trait.Deserialize.html) from a JSON
130    /// object, and must only include the fields relevant to that component.
131    pub fn get<C>(&self) -> Result<C>
132    where
133        C: for<'de> Deserialize<'de>,
134    {
135        let config = serde_json::from_value(self.config.clone())?;
136        Ok(config)
137    }
138
139    /// Adds a component-specific configuration to the configuration file.  The type `C` must be
140    /// [serializable](https://docs.rs/serde/*/serde/trait.Serialize.html) into a JSON object, and
141    /// must only include the fields relevant to that component.
142    pub fn add<C>(&mut self, config: C) -> Result<()>
143    where
144        C: Serialize,
145    {
146        let mut config = serde_json::to_value(&config)?;
147        self.config
148            .as_object_mut()
149            .unwrap()
150            .append(config.as_object_mut().unwrap());
151        Ok(())
152    }
153}