ad_editor/
config.rs

1//! A minimal config file format for ad
2use crate::{
3    buffer::Buffer,
4    editor::{Action, Actions},
5    key::Input,
6    mode::normal_mode,
7    term::{Color, Styles},
8    trie::Trie,
9    util::parent_dir_containing,
10};
11use serde::{de, Deserialize, Deserializer};
12use std::{collections::HashMap, env, fs, io, iter::successors, path::Path};
13use tracing::{error, warn};
14
15pub const DEFAULT_CONFIG: &str = include_str!("../data/config.toml");
16
17pub const TK_DEFAULT: &str = "default";
18pub const TK_DOT: &str = "dot";
19pub const TK_LOAD: &str = "load";
20pub const TK_EXEC: &str = "exec";
21
22pub(crate) fn config_path() -> String {
23    let home = env::var("HOME").unwrap();
24    format!("{home}/.ad/config.toml")
25}
26
27/// Editor level configuration
28#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
29pub struct Config {
30    pub tabstop: usize,
31    pub expand_tab: bool,
32    pub auto_mount: bool,
33    pub match_indent: bool,
34    pub status_timeout: u64,
35    pub double_click_ms: u64,
36    pub minibuffer_lines: usize,
37    pub find_command: String,
38
39    #[serde(default)]
40    pub colorscheme: ColorScheme,
41    #[serde(default)]
42    pub tree_sitter: TsConfig,
43    #[serde(default)]
44    pub languages: Vec<LangConfig>,
45    #[serde(default)]
46    pub keys: KeyBindings,
47}
48
49impl Default for Config {
50    fn default() -> Self {
51        toml::from_str(DEFAULT_CONFIG).unwrap()
52    }
53}
54
55impl Config {
56    /// Attempt to load a config file from the default location
57    pub fn try_load() -> Result<Self, String> {
58        let home = env::var("HOME").unwrap();
59        let path = config_path();
60
61        let mut cfg = match fs::read_to_string(&path) {
62            Ok(s) => match toml::from_str(&s) {
63                Ok(cfg) => cfg,
64                Err(e) => return Err(format!("Invalid config file: {e}")),
65            },
66
67            Err(e) if e.kind() == io::ErrorKind::NotFound => {
68                if fs::create_dir_all(format!("{home}/.ad")).is_ok() {
69                    if let Err(e) = fs::write(path, DEFAULT_CONFIG) {
70                        error!("unable to write default config file: {e}");
71                    }
72                }
73
74                Config::default()
75            }
76
77            Err(e) => return Err(format!("Unable to load config file: {e}")),
78        };
79
80        // Use default colorscheme's background color if none is specified
81        for style in cfg.colorscheme.syntax.values_mut() {
82            style.bg = style.bg.or(Some(cfg.colorscheme.bg));
83        }
84
85        // Replace "~/" shorthand notation in paths with the user's $HOME
86        for s in [
87            &mut cfg.tree_sitter.parser_dir,
88            &mut cfg.tree_sitter.syntax_query_dir,
89        ] {
90            if s.starts_with("~/") {
91                *s = s.replacen("~", &home, 1);
92            }
93        }
94
95        Ok(cfg)
96    }
97
98    /// Check to see if there is a known tree-sitter configuration for this buffer
99    pub fn ts_lang_for_buffer(&self, b: &Buffer) -> Option<&str> {
100        let os_ext = b.path()?.extension().unwrap_or_default();
101        let ext = os_ext.to_str().unwrap_or_default();
102        let first_line = b.line(0).map(|l| l.to_string()).unwrap_or_default();
103
104        self.languages
105            .iter()
106            .find(|c| {
107                c.extensions.iter().any(|e| e == ext)
108                    || c.first_lines.iter().any(|l| first_line.starts_with(l))
109            })
110            .map(|c| c.name.as_str())
111    }
112
113    pub(crate) fn update_from(&mut self, input: &str) -> Result<(), String> {
114        warn!("ignoring runtime config update: {input}");
115
116        Err("runtime config updates are not currently supported".to_owned())
117    }
118}
119
120/// A colorscheme for rendering the UI.
121///
122/// UI elements are available as properties and syntax stylings are available as a map of string
123/// tag to [Style]s that should be applied.
124#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
125pub struct ColorScheme {
126    pub bg: Color,
127    pub fg: Color,
128    pub bar_bg: Color,
129    pub signcol_fg: Color,
130    pub minibuffer_hl: Color,
131    pub syntax: HashMap<String, Styles>,
132}
133
134impl Default for ColorScheme {
135    fn default() -> Self {
136        let bg: Color = "#1B1720".try_into().unwrap();
137        let fg: Color = "#E6D29E".try_into().unwrap();
138        let dot_bg: Color = "#336677".try_into().unwrap();
139        let load_bg: Color = "#957FB8".try_into().unwrap();
140        let exec_bg: Color = "#Bf616A".try_into().unwrap();
141        let comment: Color = "#624354".try_into().unwrap();
142        let constant: Color = "#FF9E3B".try_into().unwrap();
143        let function: Color = "#957FB8".try_into().unwrap();
144        let keyword: Color = "#Bf616A".try_into().unwrap();
145        let module: Color = "#2D4F67".try_into().unwrap();
146        let punctuation: Color = "#9CABCA".try_into().unwrap();
147        let string: Color = "#61DCA5".try_into().unwrap();
148        let type_: Color = "#7E9CD8".try_into().unwrap();
149        let variable: Color = "#DCA561".try_into().unwrap();
150
151        #[rustfmt::skip]
152        let syntax = [
153            (TK_DEFAULT,    Styles { fg: Some(fg), bg: Some(bg), ..Default::default() }),
154            (TK_DOT,        Styles { fg: Some(fg), bg: Some(dot_bg), ..Default::default() }),
155            (TK_LOAD,       Styles { fg: Some(fg), bg: Some(load_bg), ..Default::default() }),
156            (TK_EXEC,       Styles { fg: Some(fg), bg: Some(exec_bg), ..Default::default() }),
157            ("character",   Styles { fg: Some(string), bold: true, ..Default::default() }),
158            ("comment",     Styles { fg: Some(comment), italic: true, ..Default::default() }),
159            ("constant",    Styles { fg: Some(constant), ..Default::default() }),
160            ("function",    Styles { fg: Some(function), ..Default::default() }),
161            ("keyword",     Styles { fg: Some(keyword), ..Default::default() }),
162            ("module",      Styles { fg: Some(module), ..Default::default() }),
163            ("punctuation", Styles { fg: Some(punctuation), ..Default::default() }),
164            ("string",      Styles { fg: Some(string), ..Default::default() }),
165            ("type",        Styles { fg: Some(type_), ..Default::default() }),
166            ("variable",    Styles { fg: Some(variable), ..Default::default() }),
167        ]
168        .map(|(s, v)| (s.to_string(), v))
169        .into_iter()
170        .collect();
171
172        Self {
173            bg,
174            fg,
175            bar_bg: "#4E415C".try_into().unwrap(),
176            signcol_fg: "#544863".try_into().unwrap(),
177            minibuffer_hl: "#3E3549".try_into().unwrap(),
178            syntax,
179        }
180    }
181}
182
183impl ColorScheme {
184    /// Determine UI [Styles] to be applied for a given syntax tag.
185    ///
186    /// If the full tag does not have associated styling but its dotted prefix does then the
187    /// styling of the prefix is used, otherwise default styling will be used ([TK_DEFAULT]).
188    ///
189    /// For key "foo.bar.baz" this will return the first value found out of the following keyset:
190    ///   - "foo.bar.baz"
191    ///   - "foo.bar"
192    ///   - "foo"
193    ///   - [TK_DEFAULT]
194    pub fn styles_for(&self, tag: &str) -> &Styles {
195        successors(Some(tag), |s| Some(s.rsplit_once('.')?.0))
196            .find_map(|k| self.syntax.get(k))
197            .or(self.syntax.get(TK_DEFAULT))
198            .expect("to have default styles")
199    }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
203pub struct TsConfig {
204    pub parser_dir: String,
205    pub syntax_query_dir: String,
206}
207
208impl Default for TsConfig {
209    fn default() -> Self {
210        let home = env::var("HOME").unwrap();
211
212        TsConfig {
213            parser_dir: format!("{home}/.ad/tree-sitter/parsers"),
214            syntax_query_dir: format!("{home}/.ad/tree-sitter/queries"),
215        }
216    }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
220pub struct LangConfig {
221    pub name: String,
222    pub extensions: Vec<String>,
223    #[serde(default)]
224    pub first_lines: Vec<String>,
225    #[serde(default)]
226    pub lsp: Option<LspConfig>,
227}
228
229/// Configuration for running a given language server
230#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
231pub struct LspConfig {
232    /// The command to run to start the language server
233    pub command: String,
234    /// Additional arguments to pass to the language server command
235    #[serde(default)]
236    pub args: Vec<String>,
237    /// Files or directories to search for in order to determine the project root
238    pub roots: Vec<String>,
239}
240
241impl LspConfig {
242    pub fn root_for_buffer<'a>(&self, b: &'a Buffer) -> Option<&'a Path> {
243        let d = b.dir()?;
244        for root in self.roots.iter() {
245            if let Some(p) = parent_dir_containing(d, root) {
246                return Some(p);
247            }
248        }
249
250        None
251    }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
255pub struct KeyBindings {
256    #[serde(deserialize_with = "de_serde_trie")]
257    pub normal: Trie<Input, KeyAction>,
258}
259
260impl Default for KeyBindings {
261    fn default() -> Self {
262        KeyBindings {
263            normal: Trie::from_pairs(Vec::new()).unwrap(),
264        }
265    }
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
269#[serde(untagged)]
270pub enum KeyAction {
271    External { run: String },
272}
273
274impl KeyAction {
275    pub fn as_actions(&self) -> Actions {
276        match self {
277            Self::External { run } => Actions::Single(Action::ExecuteString { s: run.clone() }),
278        }
279    }
280}
281
282pub fn de_serde_trie<'de, D>(deserializer: D) -> Result<Trie<Input, KeyAction>, D::Error>
283where
284    D: Deserializer<'de>,
285{
286    let raw_map: HashMap<String, KeyAction> = Deserialize::deserialize(deserializer)?;
287    let mut raw = Vec::with_capacity(raw_map.len());
288
289    for (k, action) in raw_map.into_iter() {
290        let keys: Vec<Input> = k
291            .split_whitespace()
292            .filter_map(|s| {
293                if s.len() == 1 {
294                    let c = s.chars().next().unwrap();
295                    if c.is_whitespace() {
296                        None
297                    } else {
298                        Some(Input::Char(c))
299                    }
300                } else {
301                    match s {
302                        "<space>" => Some(Input::Char(' ')),
303                        _ => None,
304                    }
305                }
306            })
307            .collect();
308
309        raw.push((keys, action));
310    }
311
312    // Make sure that none of the user provided bindings clash with Normal mode
313    // bindings as that will mean they never get run
314    let nm = normal_mode();
315    for (keys, _) in raw.iter() {
316        if nm.keymap.contains_key_or_prefix(keys) {
317            let mut s = String::new();
318            for k in keys {
319                if let Input::Char(c) = k {
320                    s.push(*c);
321                }
322            }
323
324            return Err(de::Error::custom(format!(
325                "mapping '{s}' collides with a Normal mode mapping"
326            )));
327        }
328    }
329
330    Trie::from_pairs(raw).map_err(de::Error::custom)
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn default_loads() {
339        Config::default(); // will panic if default config is invalid
340    }
341}