language.rs

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