ad_editor/config/
mod.rs

1//! A minimal config file format for ad
2use crate::{
3    buffer::Buffer,
4    editor::{Action, Actions},
5    key::{Arrow, Input},
6    syntax::TK_DEFAULT,
7    term::{Color, Styles},
8    trie::Trie,
9    util::parent_dir_containing,
10};
11use serde::{Deserialize, Deserializer, de};
12use std::{collections::HashMap, env, fs, iter::successors, ops::Deref, path::Path};
13use tracing::error;
14
15mod raw;
16
17use raw::{RawColorScheme, RawConfig};
18
19pub const DEFAULT_CONFIG: &str = include_str!("../../data/config.toml");
20
21pub(crate) fn config_path() -> String {
22    let home = env::var("HOME").unwrap();
23    format!("{home}/.ad/config.toml")
24}
25
26/// Editor level configuration
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Config {
29    pub editor: EditorConfig,
30    pub filesystem: FsysConfig,
31    pub tree_sitter: TsConfig,
32    pub colorscheme: ColorScheme,
33    pub filetypes: HashMap<String, FtypeConfig>,
34    pub keys: KeyBindings,
35}
36
37impl Default for Config {
38    fn default() -> Self {
39        RawConfig::default()
40            .resolve(&config_path(), "")
41            .expect("default config is broken")
42    }
43}
44
45impl Deref for Config {
46    type Target = EditorConfig;
47
48    fn deref(&self) -> &Self::Target {
49        &self.editor
50    }
51}
52
53impl Config {
54    /// Attempt to load a config file from a specified location.
55    /// If there are any errors while loading and parsing the file then they
56    /// are reported as a formatted string for displaying to the user.
57    pub fn try_load_from_path(path: &str, home: &str) -> Result<Self, String> {
58        match fs::read_to_string(path) {
59            Ok(s) => Self::try_load_from_str(&s, path, home),
60            Err(e) => Err(format!("unable to load config file: {e}")),
61        }
62    }
63
64    /// Attempt to load a config file from its raw file contents as a string.
65    /// If there are any errors while loading and parsing the file then they
66    /// are reported as a formatted string for displaying to the user.
67    pub fn try_load_from_str(s: &str, path: &str, home: &str) -> Result<Self, String> {
68        let raw: RawConfig = match toml::from_str(s) {
69            Ok(cfg) => cfg,
70            Err(e) => {
71                error!("malformed config file: {e}");
72                return Err(format!("malformed config file: {e}"));
73            }
74        };
75        let res = raw.resolve(path, home);
76        if let Err(err) = res.as_ref() {
77            error!("malformed config file: {err}");
78        }
79
80        res
81    }
82
83    /// Attempt to load a config file from the default location.
84    ///
85    /// If the config file doesn't currently exist then the default config
86    /// will be written to disk.
87    /// If there are any errors while loading and parsing the file then they
88    /// are reported as a formatted string for displaying to the user.
89    pub fn try_load() -> Result<Self, String> {
90        let home = env::var("HOME").unwrap();
91        let path = config_path();
92
93        if matches!(fs::exists(&path), Ok(false))
94            && fs::create_dir_all(format!("{home}/.ad")).is_ok()
95            && let Err(e) = fs::write(&path, DEFAULT_CONFIG)
96        {
97            error!("unable to write default config file: {e}");
98        }
99
100        Self::try_load_from_path(&path, &home)
101    }
102}
103
104/// For an explicitly provided path and first line of a file, check to see if we know the
105/// correct language associated with the file and associated [FtypeConfig].
106pub fn ftype_config_for_path_and_first_line<'a>(
107    path: &Path,
108    first_line: &str,
109    filetypes: &'a HashMap<String, FtypeConfig>,
110) -> Option<(&'a String, &'a FtypeConfig)> {
111    let fname = path.file_name()?.to_string_lossy();
112    let os_ext = path.extension().unwrap_or_default();
113    let ext = os_ext.to_str().unwrap_or_default();
114
115    filetypes.iter().find(|(_, c)| {
116        c.filenames.iter().any(|f| *f == fname)
117            || c.extensions.iter().any(|e| e == ext)
118            || c.first_lines.iter().any(|l| first_line.starts_with(l))
119    })
120}
121
122/// Top level configuration for the editor
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct EditorConfig {
125    pub show_splash: bool,
126    pub tabstop: usize,
127    pub expand_tab: bool,
128    pub match_indent: bool,
129    pub lsp_autostart: bool,
130    pub status_timeout: u64,
131    pub double_click_ms: u64,
132    pub minibuffer_lines: usize,
133    pub find_command: String,
134}
135
136impl Default for EditorConfig {
137    fn default() -> Self {
138        Self {
139            show_splash: true,
140            tabstop: 4,
141            expand_tab: true,
142            match_indent: true,
143            lsp_autostart: true,
144            status_timeout: 3,
145            double_click_ms: 200,
146            minibuffer_lines: 8,
147            find_command: "fd --hidden".to_string(),
148        }
149    }
150}
151
152/// Configuration for the 9p filesystem interface
153#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
154pub struct FsysConfig {
155    pub enabled: bool,
156    pub auto_mount: bool,
157}
158
159impl Default for FsysConfig {
160    fn default() -> Self {
161        Self {
162            enabled: true,
163            auto_mount: false,
164        }
165    }
166}
167
168/// A colorscheme for rendering the UI.
169///
170/// UI elements are available as properties and syntax stylings are available as a map of string
171/// tag to [Styles] that should be applied.
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct ColorScheme {
174    pub bg: Color,
175    pub fg: Color,
176    pub bar_bg: Color,
177    pub signcol_fg: Color,
178    pub minibuffer_hl: Color,
179    pub syntax: HashMap<String, Styles>,
180}
181
182impl Default for ColorScheme {
183    fn default() -> Self {
184        RawColorScheme::default().resolve(&mut Vec::new())
185    }
186}
187
188impl ColorScheme {
189    /// Determine UI [Styles] to be applied for a given syntax tag.
190    ///
191    /// If the full tag does not have associated styling but its dotted prefix does then the
192    /// styling of the prefix is used, otherwise default styling will be used ([TK_DEFAULT]).
193    ///
194    /// For key "foo.bar.baz" this will return the first value found out of the following keyset:
195    ///   - "foo.bar.baz"
196    ///   - "foo.bar"
197    ///   - "foo"
198    ///   - [TK_DEFAULT]
199    pub fn styles_for(&self, tag: &str) -> &Styles {
200        successors(Some(tag), |s| Some(s.rsplit_once('.')?.0))
201            .find_map(|k| self.syntax.get(k))
202            .or(self.syntax.get(TK_DEFAULT))
203            .expect("to have default styles")
204    }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
208pub struct TsConfig {
209    pub parser_dir: String,
210    pub syntax_query_dir: String,
211}
212
213impl Default for TsConfig {
214    fn default() -> Self {
215        let home = env::var("HOME").unwrap();
216
217        TsConfig {
218            parser_dir: format!("{home}/.ad/tree-sitter/parsers"),
219            syntax_query_dir: format!("{home}/.ad/tree-sitter/queries"),
220        }
221    }
222}
223
224#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
225pub struct FtypeConfig {
226    #[serde(default)]
227    pub extensions: Vec<String>,
228    #[serde(default)]
229    pub first_lines: Vec<String>,
230    #[serde(default)]
231    pub filenames: Vec<String>,
232    #[serde(default)]
233    pub re_syntax: Vec<(String, String)>,
234    #[serde(default)]
235    pub lsp: Option<LspConfig>,
236}
237
238/// Configuration for running a given language server
239#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
240pub struct LspConfig {
241    /// The command to run to start the language server
242    pub command: String,
243    /// Additional arguments to pass to the language server command
244    #[serde(default)]
245    pub args: Vec<String>,
246    /// Files or directories to search for in order to determine the project root
247    pub roots: Vec<String>,
248    /// Additional initialization options to be passed when the server is started
249    #[serde(default)]
250    pub init_opts: Option<serde_json::Value>,
251}
252
253impl LspConfig {
254    pub fn root_for_dir<'a>(&self, d: &'a Path) -> Option<&'a Path> {
255        for root in self.roots.iter() {
256            if let Some(p) = parent_dir_containing(d, root) {
257                return Some(p);
258            }
259        }
260
261        None
262    }
263
264    pub fn root_for_buffer<'a>(&self, b: &'a Buffer) -> Option<&'a Path> {
265        self.root_for_dir(b.dir()?)
266    }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
270pub struct KeyBindings {
271    #[serde(default, deserialize_with = "de_serde_trie")]
272    pub normal: Trie<Input, Actions>,
273    #[serde(default, deserialize_with = "de_serde_trie")]
274    pub insert: Trie<Input, Actions>,
275}
276
277impl Default for KeyBindings {
278    fn default() -> Self {
279        KeyBindings {
280            normal: Trie::try_from_iter(Vec::new()).unwrap(),
281            insert: Trie::try_from_iter(Vec::new()).unwrap(),
282        }
283    }
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
287#[serde(untagged)]
288pub enum KeyAction {
289    Execute { run: String },
290    Keys { send_keys: Inputs },
291}
292
293impl KeyAction {
294    fn into_actions(self) -> Actions {
295        match self {
296            Self::Execute { run } => Actions::Single(Action::ExecuteString { s: run }),
297            Self::Keys { send_keys } => Actions::Single(Action::SendKeys { ks: send_keys.0 }),
298        }
299    }
300}
301
302/// Raw inputs to be sent through to the main editor event loop
303#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
304#[serde(try_from = "String")]
305pub struct Inputs(pub(crate) Vec<Input>);
306
307impl TryFrom<String> for Inputs {
308    type Error = String;
309
310    fn try_from(value: String) -> Result<Self, Self::Error> {
311        let inputs = value
312            .split_whitespace()
313            .map(try_input_from_str_template)
314            .collect::<Result<Vec<_>, _>>()?;
315
316        if inputs.is_empty() {
317            return Err("empty key inputs string".to_owned());
318        }
319
320        Ok(Self(inputs))
321    }
322}
323
324/// Only supporting a subset of inputs for now
325fn try_input_from_str_template(s: &str) -> Result<Input, String> {
326    if s.len() == 1 {
327        Ok(Input::Char(s.chars().next().unwrap()))
328    } else if let Some(suffix) = s.strip_prefix("C-A-") {
329        if suffix.len() == 1 {
330            Ok(Input::CtrlAlt(suffix.chars().next().unwrap()))
331        } else if suffix == "<space>" {
332            Ok(Input::CtrlAlt(' '))
333        } else {
334            Err(format!("invalid send_key value: C-A-{suffix}"))
335        }
336    } else if let Some(suffix) = s.strip_prefix("C-") {
337        if suffix.len() == 1 {
338            Ok(Input::Ctrl(suffix.chars().next().unwrap()))
339        } else if suffix == "<space>" {
340            Ok(Input::Ctrl(' '))
341        } else {
342            Err(format!("invalid send_key value: C-{suffix}"))
343        }
344    } else if let Some(suffix) = s.strip_prefix("A-") {
345        if suffix.len() == 1 {
346            Ok(Input::Alt(suffix.chars().next().unwrap()))
347        } else if suffix == "<space>" {
348            Ok(Input::Alt(' '))
349        } else {
350            Err(format!("invalid send_key value: A-{suffix}"))
351        }
352    } else {
353        let i = match s {
354            "<backspace>" => Input::Backspace,
355            "<delete>" => Input::Del,
356            "<end>" => Input::End,
357            "<esc>" => Input::Esc,
358            "<home>" => Input::Home,
359            "<page-down>" => Input::PageDown,
360            "<page-up>" => Input::PageUp,
361            "<space>" => Input::Char(' '),
362            "<tab>" => Input::Tab,
363            "<left>" => Input::Arrow(Arrow::Left),
364            "<right>" => Input::Arrow(Arrow::Right),
365            "<up>" => Input::Arrow(Arrow::Up),
366            "<down>" => Input::Arrow(Arrow::Down),
367            _ => return Err(format!("unknown key {s}")),
368        };
369
370        Ok(i)
371    }
372}
373
374fn de_serde_trie<'de, D>(deserializer: D) -> Result<Trie<Input, Actions>, D::Error>
375where
376    D: Deserializer<'de>,
377{
378    let raw_map: HashMap<String, KeyAction> = Deserialize::deserialize(deserializer)?;
379    if raw_map.is_empty() {
380        return Err(de::Error::custom("empty key map"));
381    }
382
383    let raw = raw_map
384        .into_iter()
385        .map(|(k, action)| {
386            Inputs::try_from(k)
387                .map(|Inputs(keys)| (keys, action.into_actions()))
388                .map_err(de::Error::custom)
389        })
390        .collect::<Result<Vec<_>, _>>()?;
391
392    Trie::try_from_iter(raw).map_err(de::Error::custom)
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use simple_test_case::test_case;
399
400    #[test]
401    fn default_config_is_valid() {
402        Config::default(); // will panic if default config is invalid
403    }
404
405    #[test_case("A", &[Input::Char('A')]; "single letter key")]
406    #[test_case("5", &[Input::Char('5')]; "single digit key")]
407    #[test_case("A-y", &[Input::Alt('y')]; "alt letter")]
408    #[test_case("C-y", &[Input::Ctrl('y')]; "control letter")]
409    #[test_case("C-A-y", &[Input::CtrlAlt('y')]; "control alt letter")]
410    #[test_case("A-<space>", &[Input::Alt(' ')]; "alt space")]
411    #[test_case("C-<space>", &[Input::Ctrl(' ')]; "control space")]
412    #[test_case("C-A-<space>", &[Input::CtrlAlt(' ')]; "control alt space")]
413    #[test_case("<backspace>", &[Input::Backspace]; "backspace")]
414    #[test_case("<delete>", &[Input::Del]; "delete")]
415    #[test_case("<end>", &[Input::End]; "end")]
416    #[test_case("<esc>", &[Input::Esc]; "escape")]
417    #[test_case("<home>", &[Input::Home]; "home")]
418    #[test_case("<page-up>", &[Input::PageUp]; "page up")]
419    #[test_case("<page-down>", &[Input::PageDown]; "page down")]
420    #[test_case("<space>", &[Input::Char(' ')]; "space")]
421    #[test_case("<tab>", &[Input::Tab]; "tab")]
422    #[test_case("<left>", &[Input::Arrow(Arrow::Left)]; "left")]
423    #[test_case("<right>", &[Input::Arrow(Arrow::Right)]; "right")]
424    #[test_case("<up>", &[Input::Arrow(Arrow::Up)]; "up")]
425    #[test_case("<down>", &[Input::Arrow(Arrow::Down)]; "down")]
426    #[test_case("A B C", &[Input::Char('A'), Input::Char('B'), Input::Char('C')]; "sequence")]
427    #[test]
428    fn inputs_try_from_string_works(raw: &str, expected: &[Input]) {
429        let inputs = Inputs::try_from(raw.to_owned()).unwrap();
430        assert_eq!(&inputs.0, expected);
431    }
432
433    #[test]
434    fn inputs_try_from_empty_string_errors() {
435        assert_eq!(
436            &Inputs::try_from(String::new()).unwrap_err(),
437            "empty key inputs string"
438        );
439    }
440
441    #[test]
442    fn parsing_an_empty_keymap_errors() {
443        let res: Result<KeyBindings, _> = toml::from_str("[normal]");
444        assert!(res.is_err());
445    }
446}