syntastica_parsers_dynamic/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(
3    feature = "docs",
4    cfg_attr(doc, doc = ::document_features::document_features!(feature_label = r#"<span class="stab portability"><code>{feature}</code></span>"#))
5)]
6#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))]
7#![warn(rust_2018_idioms)]
8#![deny(missing_docs)]
9
10use std::{
11    borrow::Cow,
12    cell::RefCell,
13    collections::HashMap,
14    fmt::{self, Debug, Formatter},
15    marker::PhantomData,
16    path::PathBuf,
17};
18
19use anyhow::Result;
20use syntastica_core::language_set::{
21    FileType, HighlightConfiguration, LanguageSet, SupportedLanguage,
22};
23
24use loader::{Config, Loader};
25
26mod loader;
27
28/// A successfully loaded language.
29///
30/// Instances can only be obtained through the functions of the [`SupportedLanguage`] trait, and
31/// they are tied to a [`LanguageLoader`] through a lifetime. The loader is responsible for actually
32/// locating and initializing the languages.
33#[derive(PartialEq, Eq, Hash, Clone)]
34pub struct Lang<'loader>(Box<str>, PhantomData<&'loader ()>);
35
36impl Debug for Lang<'_> {
37    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
38        f.debug_tuple("Lang").field(&self.0).finish()
39    }
40}
41
42impl<'loader> SupportedLanguage<'loader, LanguageLoader> for Lang<'loader> {
43    fn name(&self) -> Cow<'_, str> {
44        self.0.as_ref().into()
45    }
46
47    fn for_name(
48        name: impl AsRef<str>,
49        set: &'loader LanguageLoader,
50    ) -> syntastica_core::Result<Self> {
51        let name = name.as_ref();
52        if set.highlight_configs.borrow().contains_key(name) {
53            return Ok(Self(name.into(), PhantomData));
54        }
55
56        let (lang, lang_config) = set
57            .loader
58            .language_configuration_for_name(name)
59            .map_err(|err| syntastica_core::Error::Custom(err.to_string()))?
60            .ok_or_else(|| syntastica_core::Error::UnsupportedLanguage(name.to_string()))?;
61
62        let config_ref = lang_config
63            // TODO: allow custom query paths
64            .highlight_config(lang, None)?
65            .ok_or_else(|| syntastica_core::Error::UnsupportedLanguage(name.to_string()))?;
66
67        let name = Box::<str>::from(name);
68        set.highlight_configs
69            .borrow_mut()
70            .insert(name.clone(), config_ref);
71        Ok(Self(name, PhantomData))
72    }
73
74    fn for_file_type(_file_type: FileType, _set: &'loader LanguageLoader) -> Option<Self> {
75        // TODO: detection by filetype when tft allows getting a file name/extension from a
76        // FileType instance
77        None
78    }
79
80    fn for_injection(name: impl AsRef<str>, set: &'loader LanguageLoader) -> Option<Self> {
81        let (lang, lang_config) = set
82            .loader
83            .language_configuration_for_injection_string(name.as_ref())
84            .ok()??;
85        let name = lang_config.language_name.as_str();
86
87        if set.highlight_configs.borrow().contains_key(name) {
88            return Some(Self(name.into(), PhantomData));
89        }
90
91        let config_ref = lang_config
92            // TODO: allow custom query paths
93            .highlight_config(lang, None)
94            .ok()??;
95
96        let name = Box::<str>::from(name);
97        set.highlight_configs
98            .borrow_mut()
99            .insert(name.clone(), config_ref);
100        Some(Self(name, PhantomData))
101    }
102}
103
104/// A [`LanguageSet`] implementation that loads languages dynamically at runtime.
105pub struct LanguageLoader {
106    loader: Loader,
107    highlight_configs: RefCell<HashMap<Box<str>, &'static HighlightConfiguration>>,
108}
109
110impl LanguageLoader {
111    /// Create a new [`LanguageLoader`] which searches for parsers in `parser_search_dirs`.
112    ///
113    /// The directories are scanned once during creation.
114    pub fn new(parser_search_dirs: Vec<PathBuf>) -> Result<Self> {
115        let mut loader = Loader::new()?;
116        loader.configure_highlights(syntastica_core::theme::THEME_KEYS);
117        loader.find_all_languages(&Config {
118            parser_directories: parser_search_dirs,
119        })?;
120        Ok(Self {
121            loader,
122            highlight_configs: RefCell::new(HashMap::new()),
123        })
124    }
125}
126
127impl<'loader> LanguageSet<'loader> for LanguageLoader {
128    type Language = Lang<'loader>;
129
130    fn get_language(
131        &self,
132        language: Self::Language,
133    ) -> syntastica_core::Result<&HighlightConfiguration> {
134        Ok(self
135            .highlight_configs
136            .borrow()
137            .get(&language.0)
138            .expect("Lang instances should initialize highlight configs"))
139    }
140}
141
142impl Drop for LanguageLoader {
143    fn drop(&mut self) {
144        for (_, config_ref) in self.highlight_configs.borrow_mut().drain() {
145            // SAFETY: references were created using `Box::leak` and are only given out with the
146            // lifetime of `&self`
147            drop(unsafe { Box::from_raw(config_ref as *const _ as *mut HighlightConfiguration) });
148        }
149    }
150}