1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::Path;
4use std::sync::LazyLock;
5
6#[doc = include_str!("docs/language_catalog.md")]
7#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
8pub enum LanguageId {
9 Rust,
10 Python,
11 JavaScript,
12 JavaScriptReact,
13 TypeScript,
14 TypeScriptReact,
15 Go,
16 Java,
17 C,
18 Cpp,
19 CSharp,
20 Ruby,
21 Php,
22 Swift,
23 Kotlin,
24 Scala,
25 Html,
26 Css,
27 Json,
28 Yaml,
29 Toml,
30 Markdown,
31 Xml,
32 Sql,
33 ShellScript,
34 PlainText,
35}
36
37impl LanguageId {
38 pub fn as_str(self) -> &'static str {
40 match self {
41 Self::Rust => "rust",
42 Self::Python => "python",
43 Self::JavaScript => "javascript",
44 Self::JavaScriptReact => "javascriptreact",
45 Self::TypeScript => "typescript",
46 Self::TypeScriptReact => "typescriptreact",
47 Self::Go => "go",
48 Self::Java => "java",
49 Self::C => "c",
50 Self::Cpp => "cpp",
51 Self::CSharp => "csharp",
52 Self::Ruby => "ruby",
53 Self::Php => "php",
54 Self::Swift => "swift",
55 Self::Kotlin => "kotlin",
56 Self::Scala => "scala",
57 Self::Html => "html",
58 Self::Css => "css",
59 Self::Json => "json",
60 Self::Yaml => "yaml",
61 Self::Toml => "toml",
62 Self::Markdown => "markdown",
63 Self::Xml => "xml",
64 Self::Sql => "sql",
65 Self::ShellScript => "shellscript",
66 Self::PlainText => "plaintext",
67 }
68 }
69
70 pub fn from_extension(ext: &str) -> Option<Self> {
72 from_extension(ext)
73 }
74
75 pub fn from_path(path: &Path) -> Self {
79 path.extension().and_then(|e| e.to_str()).and_then(Self::from_extension).unwrap_or(Self::PlainText)
80 }
81
82 pub fn primary_extension(self) -> Option<&'static str> {
83 metadata_for(self).and_then(|metadata| metadata.primary_extension)
84 }
85}
86
87#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
88pub(crate) enum ServerKind {
89 RustAnalyzer,
90 TypeScriptLanguageServer,
91 Pyright,
92 Gopls,
93 Clangd,
94}
95
96impl ServerKind {
97 pub(crate) fn as_str(self) -> &'static str {
98 match self {
99 Self::RustAnalyzer => "rust-analyzer",
100 Self::TypeScriptLanguageServer => "typescript-language-server",
101 Self::Pyright => "pyright-langserver",
102 Self::Gopls => "gopls",
103 Self::Clangd => "clangd",
104 }
105 }
106
107 fn env_key(self) -> &'static str {
108 match self {
109 Self::RustAnalyzer => "RUST_ANALYZER",
110 Self::TypeScriptLanguageServer => "TYPESCRIPT_LANGUAGE_SERVER",
111 Self::Pyright => "PYRIGHT",
112 Self::Gopls => "GOPLS",
113 Self::Clangd => "CLANGD",
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy)]
119pub struct LanguageMetadata {
120 pub id: LanguageId,
121 pub primary_extension: Option<&'static str>,
122 pub aliases: &'static [&'static str],
123 pub extensions: &'static [&'static str],
124}
125
126#[derive(Debug, Clone)]
127pub struct LspConfig {
128 pub command: String,
129 pub args: Vec<String>,
130 pub languages: Vec<LanguageId>,
131}
132
133impl LspConfig {
134 pub fn new(command: impl Into<String>) -> Self {
135 Self { command: command.into(), args: Vec::new(), languages: Vec::new() }
136 }
137
138 pub fn with_args(mut self, args: Vec<String>) -> Self {
139 self.args = args;
140 self
141 }
142
143 pub fn with_languages(mut self, languages: Vec<LanguageId>) -> Self {
144 self.languages = languages;
145 self
146 }
147}
148
149#[derive(Clone, Copy)]
150struct ServerSpec {
151 kind: ServerKind,
152 command: &'static str,
153 args: &'static [&'static str],
154}
155
156#[derive(Clone, Copy)]
157struct LanguageSpec {
158 metadata: LanguageMetadata,
159 server_kind: Option<ServerKind>,
160}
161
162const SERVER_SPECS: &[ServerSpec] = &[
163 ServerSpec { kind: ServerKind::RustAnalyzer, command: "rust-analyzer", args: &[] },
164 ServerSpec {
165 kind: ServerKind::TypeScriptLanguageServer,
166 command: "typescript-language-server",
167 args: &["--stdio"],
168 },
169 ServerSpec { kind: ServerKind::Pyright, command: "pyright-langserver", args: &["--stdio"] },
170 ServerSpec { kind: ServerKind::Gopls, command: "gopls", args: &[] },
171 ServerSpec { kind: ServerKind::Clangd, command: "clangd", args: &[] },
172];
173
174const LANGUAGE_SPECS: &[LanguageSpec] = &[
175 LanguageSpec {
176 metadata: LanguageMetadata {
177 id: LanguageId::Rust,
178 primary_extension: Some("rs"),
179 aliases: &["rust", "rs"],
180 extensions: &["rs"],
181 },
182 server_kind: Some(ServerKind::RustAnalyzer),
183 },
184 LanguageSpec {
185 metadata: LanguageMetadata {
186 id: LanguageId::Python,
187 primary_extension: Some("py"),
188 aliases: &["python", "py"],
189 extensions: &["py", "pyi", "pyw"],
190 },
191 server_kind: Some(ServerKind::Pyright),
192 },
193 LanguageSpec {
194 metadata: LanguageMetadata {
195 id: LanguageId::JavaScript,
196 primary_extension: Some("js"),
197 aliases: &["javascript", "js"],
198 extensions: &["js", "mjs"],
199 },
200 server_kind: Some(ServerKind::TypeScriptLanguageServer),
201 },
202 LanguageSpec {
203 metadata: LanguageMetadata {
204 id: LanguageId::JavaScriptReact,
205 primary_extension: Some("jsx"),
206 aliases: &["javascript", "js", "javascriptreact", "jsx"],
207 extensions: &["jsx"],
208 },
209 server_kind: Some(ServerKind::TypeScriptLanguageServer),
210 },
211 LanguageSpec {
212 metadata: LanguageMetadata {
213 id: LanguageId::TypeScript,
214 primary_extension: Some("ts"),
215 aliases: &["typescript", "ts"],
216 extensions: &["ts"],
217 },
218 server_kind: Some(ServerKind::TypeScriptLanguageServer),
219 },
220 LanguageSpec {
221 metadata: LanguageMetadata {
222 id: LanguageId::TypeScriptReact,
223 primary_extension: Some("tsx"),
224 aliases: &["typescript", "ts", "typescriptreact", "tsx"],
225 extensions: &["tsx"],
226 },
227 server_kind: Some(ServerKind::TypeScriptLanguageServer),
228 },
229 LanguageSpec {
230 metadata: LanguageMetadata {
231 id: LanguageId::Go,
232 primary_extension: Some("go"),
233 aliases: &["go"],
234 extensions: &["go"],
235 },
236 server_kind: Some(ServerKind::Gopls),
237 },
238 LanguageSpec {
239 metadata: LanguageMetadata {
240 id: LanguageId::Java,
241 primary_extension: Some("java"),
242 aliases: &["java"],
243 extensions: &["java"],
244 },
245 server_kind: None,
246 },
247 LanguageSpec {
248 metadata: LanguageMetadata {
249 id: LanguageId::C,
250 primary_extension: Some("c"),
251 aliases: &["c"],
252 extensions: &["c", "h"],
253 },
254 server_kind: Some(ServerKind::Clangd),
255 },
256 LanguageSpec {
257 metadata: LanguageMetadata {
258 id: LanguageId::Cpp,
259 primary_extension: Some("cpp"),
260 aliases: &["cpp", "c++"],
261 extensions: &["cpp", "cxx", "cc", "hpp", "hxx", "hh"],
262 },
263 server_kind: Some(ServerKind::Clangd),
264 },
265 LanguageSpec {
266 metadata: LanguageMetadata {
267 id: LanguageId::CSharp,
268 primary_extension: Some("cs"),
269 aliases: &["csharp", "cs"],
270 extensions: &["cs"],
271 },
272 server_kind: None,
273 },
274 LanguageSpec {
275 metadata: LanguageMetadata {
276 id: LanguageId::Ruby,
277 primary_extension: Some("rb"),
278 aliases: &["ruby", "rb"],
279 extensions: &["rb"],
280 },
281 server_kind: None,
282 },
283 LanguageSpec {
284 metadata: LanguageMetadata {
285 id: LanguageId::Php,
286 primary_extension: Some("php"),
287 aliases: &["php"],
288 extensions: &["php"],
289 },
290 server_kind: None,
291 },
292 LanguageSpec {
293 metadata: LanguageMetadata {
294 id: LanguageId::Swift,
295 primary_extension: Some("swift"),
296 aliases: &["swift"],
297 extensions: &["swift"],
298 },
299 server_kind: None,
300 },
301 LanguageSpec {
302 metadata: LanguageMetadata {
303 id: LanguageId::Kotlin,
304 primary_extension: Some("kt"),
305 aliases: &["kotlin"],
306 extensions: &["kt", "kts"],
307 },
308 server_kind: None,
309 },
310 LanguageSpec {
311 metadata: LanguageMetadata {
312 id: LanguageId::Scala,
313 primary_extension: Some("scala"),
314 aliases: &["scala"],
315 extensions: &["scala"],
316 },
317 server_kind: None,
318 },
319 LanguageSpec {
320 metadata: LanguageMetadata {
321 id: LanguageId::Html,
322 primary_extension: Some("html"),
323 aliases: &["html"],
324 extensions: &["html", "htm"],
325 },
326 server_kind: None,
327 },
328 LanguageSpec {
329 metadata: LanguageMetadata {
330 id: LanguageId::Css,
331 primary_extension: Some("css"),
332 aliases: &["css"],
333 extensions: &["css"],
334 },
335 server_kind: None,
336 },
337 LanguageSpec {
338 metadata: LanguageMetadata {
339 id: LanguageId::Json,
340 primary_extension: Some("json"),
341 aliases: &["json"],
342 extensions: &["json"],
343 },
344 server_kind: None,
345 },
346 LanguageSpec {
347 metadata: LanguageMetadata {
348 id: LanguageId::Yaml,
349 primary_extension: Some("yaml"),
350 aliases: &["yaml", "yml"],
351 extensions: &["yaml", "yml"],
352 },
353 server_kind: None,
354 },
355 LanguageSpec {
356 metadata: LanguageMetadata {
357 id: LanguageId::Toml,
358 primary_extension: Some("toml"),
359 aliases: &["toml"],
360 extensions: &["toml"],
361 },
362 server_kind: None,
363 },
364 LanguageSpec {
365 metadata: LanguageMetadata {
366 id: LanguageId::Markdown,
367 primary_extension: Some("md"),
368 aliases: &["markdown", "md"],
369 extensions: &["md", "markdown"],
370 },
371 server_kind: None,
372 },
373 LanguageSpec {
374 metadata: LanguageMetadata {
375 id: LanguageId::Xml,
376 primary_extension: Some("xml"),
377 aliases: &["xml"],
378 extensions: &["xml"],
379 },
380 server_kind: None,
381 },
382 LanguageSpec {
383 metadata: LanguageMetadata {
384 id: LanguageId::Sql,
385 primary_extension: Some("sql"),
386 aliases: &["sql"],
387 extensions: &["sql"],
388 },
389 server_kind: None,
390 },
391 LanguageSpec {
392 metadata: LanguageMetadata {
393 id: LanguageId::ShellScript,
394 primary_extension: Some("sh"),
395 aliases: &["sh", "shell", "bash"],
396 extensions: &["sh", "bash", "zsh"],
397 },
398 server_kind: None,
399 },
400 LanguageSpec {
401 metadata: LanguageMetadata {
402 id: LanguageId::PlainText,
403 primary_extension: None,
404 aliases: &["plaintext", "text", "txt"],
405 extensions: &["txt"],
406 },
407 server_kind: None,
408 },
409];
410
411pub static LANGUAGE_METADATA: LazyLock<Vec<LanguageMetadata>> =
412 LazyLock::new(|| LANGUAGE_SPECS.iter().map(|spec| spec.metadata).collect());
413
414static CONFIG_MAP: LazyLock<HashMap<LanguageId, LspConfig>> = LazyLock::new(|| {
415 let languages_by_server: HashMap<ServerKind, Vec<LanguageId>> = LANGUAGE_SPECS
416 .iter()
417 .filter_map(|spec| spec.server_kind.map(|kind| (kind, spec.metadata.id)))
418 .fold(HashMap::new(), |mut acc, (kind, id)| {
419 acc.entry(kind).or_default().push(id);
420 acc
421 });
422
423 LANGUAGE_SPECS
424 .iter()
425 .filter_map(|spec| {
426 let server_kind = spec.server_kind?;
427 let server = SERVER_SPECS.iter().find(|server| server.kind == server_kind)?;
428 Some((
429 spec.metadata.id,
430 LspConfig::new(server.command)
431 .with_args(server.args.iter().map(|arg| (*arg).to_string()).collect())
432 .with_languages(languages_by_server.get(&server_kind).cloned().unwrap_or_default()),
433 ))
434 })
435 .collect()
436});
437
438pub(crate) fn server_kind_for_language(id: LanguageId) -> Option<ServerKind> {
439 LANGUAGE_SPECS.iter().find(|spec| spec.metadata.id == id).and_then(|spec| spec.server_kind)
440}
441
442pub(crate) fn socket_identity_for_language(id: LanguageId) -> &'static str {
443 server_kind_for_language(id).map_or_else(|| id.as_str(), ServerKind::as_str)
444}
445
446pub(crate) fn resolved_config_for_language(language: LanguageId) -> Option<LspConfig> {
447 let mut config = get_config_for_language(language)?.clone();
448 let server_kind = server_kind_for_language(language)?;
449
450 let command_key = format!("AETHER_LSPD_SERVER_COMMAND_{}", server_kind.env_key());
451 if let Some(command) = std::env::var_os(command_key) {
452 config.command = command.to_string_lossy().into_owned();
453 }
454
455 let args_key = format!("AETHER_LSPD_SERVER_ARGS_{}", server_kind.env_key());
456 if let Ok(args) = std::env::var(args_key)
457 && let Ok(parsed) = serde_json::from_str::<Vec<String>>(&args)
458 {
459 config.args = parsed;
460 }
461
462 Some(config)
463}
464
465pub(crate) fn from_extension(ext: &str) -> Option<LanguageId> {
466 LANGUAGE_SPECS.iter().find(|spec| spec.metadata.extensions.contains(&ext)).map(|spec| spec.metadata.id)
467}
468
469pub fn metadata_for(id: LanguageId) -> Option<&'static LanguageMetadata> {
470 LANGUAGE_METADATA.iter().find(|metadata| metadata.id == id)
471}
472
473pub fn from_lsp_id(lsp_id: &str) -> Option<LanguageId> {
474 LANGUAGE_SPECS.iter().find(|spec| spec.metadata.id.as_str() == lsp_id).map(|spec| spec.metadata.id)
475}
476
477pub fn extensions_for_alias(alias: &str) -> Vec<&'static str> {
478 let lower = alias.to_lowercase();
479 LANGUAGE_SPECS
480 .iter()
481 .filter(|spec| spec.metadata.aliases.iter().any(|candidate| *candidate == lower))
482 .flat_map(|spec| spec.metadata.extensions.iter().copied())
483 .collect()
484}
485
486pub fn get_config_for_language(language: LanguageId) -> Option<&'static LspConfig> {
487 CONFIG_MAP.get(&language)
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn typescript_family_shares_server_kind() {
496 assert_eq!(
497 server_kind_for_language(LanguageId::TypeScript),
498 server_kind_for_language(LanguageId::TypeScriptReact)
499 );
500 assert_eq!(
501 socket_identity_for_language(LanguageId::TypeScript),
502 socket_identity_for_language(LanguageId::TypeScriptReact)
503 );
504 }
505
506 #[test]
507 fn c_family_shares_server_kind() {
508 assert_eq!(server_kind_for_language(LanguageId::C), server_kind_for_language(LanguageId::Cpp));
509 assert_eq!(socket_identity_for_language(LanguageId::C), socket_identity_for_language(LanguageId::Cpp));
510 }
511
512 #[test]
513 fn metadata_for_returns_correct_data() {
514 let meta = metadata_for(LanguageId::Rust).unwrap();
515 assert_eq!(meta.id.as_str(), "rust");
516 assert_eq!(meta.primary_extension, Some("rs"));
517 assert!(meta.aliases.contains(&"rust"));
518 assert!(meta.aliases.contains(&"rs"));
519 }
520
521 #[test]
522 fn from_lsp_id_resolves_known_languages() {
523 assert_eq!(from_lsp_id("rust"), Some(LanguageId::Rust));
524 assert_eq!(from_lsp_id("typescriptreact"), Some(LanguageId::TypeScriptReact));
525 assert_eq!(from_lsp_id("unknown"), None);
526 }
527
528 #[test]
529 fn primary_extension_delegates_to_catalog() {
530 assert_eq!(LanguageId::Rust.primary_extension(), Some("rs"));
531 assert_eq!(LanguageId::Python.primary_extension(), Some("py"));
532 assert_eq!(LanguageId::PlainText.primary_extension(), None);
533 }
534
535 #[test]
536 fn extensions_for_alias_includes_related_variants() {
537 let js_exts = extensions_for_alias("javascript");
538 assert!(js_exts.contains(&"js"));
539 assert!(js_exts.contains(&"mjs"));
540 assert!(js_exts.contains(&"jsx"));
541
542 let ts_exts = extensions_for_alias("typescript");
543 assert!(ts_exts.contains(&"ts"));
544 assert!(ts_exts.contains(&"tsx"));
545
546 let sh_exts = extensions_for_alias("bash");
547 assert!(sh_exts.contains(&"sh"));
548 assert!(sh_exts.contains(&"bash"));
549 assert!(sh_exts.contains(&"zsh"));
550 }
551
552 #[test]
553 fn all_languages_have_metadata() {
554 let variants = [
555 LanguageId::Rust,
556 LanguageId::Python,
557 LanguageId::JavaScript,
558 LanguageId::JavaScriptReact,
559 LanguageId::TypeScript,
560 LanguageId::TypeScriptReact,
561 LanguageId::Go,
562 LanguageId::Java,
563 LanguageId::C,
564 LanguageId::Cpp,
565 LanguageId::CSharp,
566 LanguageId::Ruby,
567 LanguageId::Php,
568 LanguageId::Swift,
569 LanguageId::Kotlin,
570 LanguageId::Scala,
571 LanguageId::Html,
572 LanguageId::Css,
573 LanguageId::Json,
574 LanguageId::Yaml,
575 LanguageId::Toml,
576 LanguageId::Markdown,
577 LanguageId::Xml,
578 LanguageId::Sql,
579 LanguageId::ShellScript,
580 LanguageId::PlainText,
581 ];
582
583 for variant in variants {
584 assert!(metadata_for(variant).is_some(), "Missing metadata for {variant:?}");
585 }
586 }
587
588 #[test]
589 fn language_id_as_str() {
590 assert_eq!(LanguageId::Rust.as_str(), "rust");
591 assert_eq!(LanguageId::TypeScriptReact.as_str(), "typescriptreact");
592 }
593
594 #[test]
595 fn language_id_from_extension() {
596 assert_eq!(LanguageId::from_extension("rs"), Some(LanguageId::Rust));
597 assert_eq!(LanguageId::from_extension("tsx"), Some(LanguageId::TypeScriptReact));
598 assert_eq!(LanguageId::from_extension("xyz"), None);
599 }
600
601 #[test]
602 fn language_id_from_path() {
603 assert_eq!(LanguageId::from_path(Path::new("foo.rs")), LanguageId::Rust);
604 assert_eq!(LanguageId::from_path(Path::new("bar.py")), LanguageId::Python);
605 assert_eq!(LanguageId::from_path(Path::new("baz.tsx")), LanguageId::TypeScriptReact);
606 assert_eq!(LanguageId::from_path(Path::new("unknown.xyz")), LanguageId::PlainText);
607 assert_eq!(LanguageId::from_path(Path::new("no_extension")), LanguageId::PlainText);
608 }
609
610 #[test]
611 fn lsp_config_builder() {
612 let config = LspConfig::new("test-lsp")
613 .with_args(vec!["--stdio".to_string(), "--debug".to_string()])
614 .with_languages(vec![LanguageId::Rust]);
615
616 assert_eq!(config.command, "test-lsp");
617 assert_eq!(config.args, vec!["--stdio", "--debug"]);
618 assert_eq!(config.languages, vec![LanguageId::Rust]);
619 }
620
621 #[test]
622 fn get_config_for_known_languages() {
623 let rust_config = get_config_for_language(LanguageId::Rust);
624 assert!(rust_config.is_some());
625 assert_eq!(rust_config.unwrap().command, "rust-analyzer");
626
627 let ts_config = get_config_for_language(LanguageId::TypeScript);
628 assert!(ts_config.is_some());
629 assert_eq!(ts_config.unwrap().command, "typescript-language-server");
630
631 let plaintext_config = get_config_for_language(LanguageId::PlainText);
632 assert!(plaintext_config.is_none());
633 }
634}