language.rs

  1use crate::HighlightMap;
  2use anyhow::Result;
  3use gpui::AppContext;
  4use parking_lot::Mutex;
  5use serde::Deserialize;
  6use std::{collections::HashSet, path::Path, str, sync::Arc};
  7use theme::SyntaxTheme;
  8use tree_sitter::{Language as Grammar, Query};
  9pub use tree_sitter::{Parser, Tree};
 10
 11#[derive(Default, Deserialize)]
 12pub struct LanguageConfig {
 13    pub name: String,
 14    pub path_suffixes: Vec<String>,
 15    pub brackets: Vec<BracketPair>,
 16    pub language_server: Option<LanguageServerConfig>,
 17}
 18
 19#[derive(Deserialize)]
 20pub struct LanguageServerConfig {
 21    pub binary: String,
 22    pub disk_based_diagnostic_sources: HashSet<String>,
 23}
 24
 25#[derive(Clone, Debug, Deserialize)]
 26pub struct BracketPair {
 27    pub start: String,
 28    pub end: String,
 29    pub close: bool,
 30    pub newline: bool,
 31}
 32
 33pub struct Language {
 34    pub(crate) config: LanguageConfig,
 35    pub(crate) grammar: Grammar,
 36    pub(crate) highlights_query: Query,
 37    pub(crate) brackets_query: Query,
 38    pub(crate) indents_query: Query,
 39    pub(crate) highlight_map: Mutex<HighlightMap>,
 40}
 41
 42#[derive(Default)]
 43pub struct LanguageRegistry {
 44    languages: Vec<Arc<Language>>,
 45}
 46
 47impl LanguageRegistry {
 48    pub fn new() -> Self {
 49        Self::default()
 50    }
 51
 52    pub fn add(&mut self, language: Arc<Language>) {
 53        self.languages.push(language);
 54    }
 55
 56    pub fn set_theme(&self, theme: &SyntaxTheme) {
 57        for language in &self.languages {
 58            language.set_theme(theme);
 59        }
 60    }
 61
 62    pub fn get_language(&self, name: &str) -> Option<&Arc<Language>> {
 63        self.languages
 64            .iter()
 65            .find(|language| language.name() == name)
 66    }
 67
 68    pub fn select_language(&self, path: impl AsRef<Path>) -> Option<&Arc<Language>> {
 69        let path = path.as_ref();
 70        let filename = path.file_name().and_then(|name| name.to_str());
 71        let extension = path.extension().and_then(|name| name.to_str());
 72        let path_suffixes = [extension, filename];
 73        self.languages.iter().find(|language| {
 74            language
 75                .config
 76                .path_suffixes
 77                .iter()
 78                .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
 79        })
 80    }
 81}
 82
 83impl Language {
 84    pub fn new(config: LanguageConfig, grammar: Grammar) -> Self {
 85        Self {
 86            config,
 87            brackets_query: Query::new(grammar, "").unwrap(),
 88            highlights_query: Query::new(grammar, "").unwrap(),
 89            indents_query: Query::new(grammar, "").unwrap(),
 90            grammar,
 91            highlight_map: Default::default(),
 92        }
 93    }
 94
 95    pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
 96        self.highlights_query = Query::new(self.grammar, source)?;
 97        Ok(self)
 98    }
 99
100    pub fn with_brackets_query(mut self, source: &str) -> Result<Self> {
101        self.brackets_query = Query::new(self.grammar, source)?;
102        Ok(self)
103    }
104
105    pub fn with_indents_query(mut self, source: &str) -> Result<Self> {
106        self.indents_query = Query::new(self.grammar, source)?;
107        Ok(self)
108    }
109
110    pub fn name(&self) -> &str {
111        self.config.name.as_str()
112    }
113
114    pub fn start_server(
115        &self,
116        root_path: &Path,
117        cx: &AppContext,
118    ) -> Result<Option<Arc<lsp::LanguageServer>>> {
119        if let Some(config) = &self.config.language_server {
120            const ZED_BUNDLE: Option<&'static str> = option_env!("ZED_BUNDLE");
121            let binary_path = if ZED_BUNDLE.map_or(Ok(false), |b| b.parse())? {
122                cx.platform()
123                    .path_for_resource(Some(&config.binary), None)?
124            } else {
125                Path::new(&config.binary).to_path_buf()
126            };
127            lsp::LanguageServer::new(&binary_path, root_path, cx.background().clone()).map(Some)
128        } else {
129            Ok(None)
130        }
131    }
132
133    pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {
134        self.config
135            .language_server
136            .as_ref()
137            .map(|config| &config.disk_based_diagnostic_sources)
138    }
139
140    pub fn brackets(&self) -> &[BracketPair] {
141        &self.config.brackets
142    }
143
144    pub fn highlight_map(&self) -> HighlightMap {
145        self.highlight_map.lock().clone()
146    }
147
148    pub fn set_theme(&self, theme: &SyntaxTheme) {
149        *self.highlight_map.lock() =
150            HighlightMap::new(self.highlights_query.capture_names(), theme);
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_select_language() {
160        let grammar = tree_sitter_rust::language();
161        let registry = LanguageRegistry {
162            languages: vec![
163                Arc::new(Language::new(
164                    LanguageConfig {
165                        name: "Rust".to_string(),
166                        path_suffixes: vec!["rs".to_string()],
167                        ..Default::default()
168                    },
169                    grammar,
170                )),
171                Arc::new(Language::new(
172                    LanguageConfig {
173                        name: "Make".to_string(),
174                        path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
175                        ..Default::default()
176                    },
177                    grammar,
178                )),
179            ],
180        };
181
182        // matching file extension
183        assert_eq!(
184            registry.select_language("zed/lib.rs").map(|l| l.name()),
185            Some("Rust")
186        );
187        assert_eq!(
188            registry.select_language("zed/lib.mk").map(|l| l.name()),
189            Some("Make")
190        );
191
192        // matching filename
193        assert_eq!(
194            registry.select_language("zed/Makefile").map(|l| l.name()),
195            Some("Make")
196        );
197
198        // matching suffix that is not the full file extension or filename
199        assert_eq!(registry.select_language("zed/cars").map(|l| l.name()), None);
200        assert_eq!(
201            registry.select_language("zed/a.cars").map(|l| l.name()),
202            None
203        );
204        assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None);
205    }
206}