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