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}