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 pub(crate) 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 LanguageServerMetadata {
120 pub display_name: &'static str,
121 pub installation_instructions: Option<&'static str>,
122}
123
124#[derive(Debug, Clone, Copy)]
125pub struct LanguageMetadata {
126 pub id: LanguageId,
127 pub primary_extension: Option<&'static str>,
128 pub aliases: &'static [&'static str],
129 pub extensions: &'static [&'static str],
130}
131
132#[derive(Debug, Clone)]
133pub struct LspConfig {
134 pub command: String,
135 pub args: Vec<String>,
136 pub languages: Vec<LanguageId>,
137}
138
139impl LspConfig {
140 pub fn new(command: impl Into<String>) -> Self {
141 Self { command: command.into(), args: Vec::new(), languages: Vec::new() }
142 }
143
144 pub fn with_args(mut self, args: Vec<String>) -> Self {
145 self.args = args;
146 self
147 }
148
149 pub fn with_languages(mut self, languages: Vec<LanguageId>) -> Self {
150 self.languages = languages;
151 self
152 }
153}
154
155#[derive(Clone, Copy)]
156struct ServerSpec {
157 kind: ServerKind,
158 command: &'static str,
159 display_name: &'static str,
160 args: &'static [&'static str],
161 installation_instructions: Option<&'static str>,
162}
163
164#[derive(Clone, Copy)]
165struct LanguageSpec {
166 metadata: LanguageMetadata,
167 server_kind: Option<ServerKind>,
168}
169
170const SERVER_SPECS: &[ServerSpec] = &[
171 ServerSpec {
172 kind: ServerKind::RustAnalyzer,
173 command: "rust-analyzer",
174 display_name: "rust-analyzer",
175 args: &[],
176 installation_instructions: None,
177 },
178 ServerSpec {
179 kind: ServerKind::TypeScriptLanguageServer,
180 command: "typescript-language-server",
181 display_name: "TypeScript language server",
182 args: &["--stdio"],
183 installation_instructions: Some(
184 "Install it in this workspace with `npm install --save-dev typescript typescript-language-server`, or \
185 install it globally with `npm install --global typescript typescript-language-server`, then retry.",
186 ),
187 },
188 ServerSpec {
189 kind: ServerKind::Pyright,
190 command: "pyright-langserver",
191 display_name: "Pyright language server",
192 args: &["--stdio"],
193 installation_instructions: None,
194 },
195 ServerSpec {
196 kind: ServerKind::Gopls,
197 command: "gopls",
198 display_name: "gopls language server",
199 args: &[],
200 installation_instructions: None,
201 },
202 ServerSpec {
203 kind: ServerKind::Clangd,
204 command: "clangd",
205 display_name: "clangd language server",
206 args: &[],
207 installation_instructions: None,
208 },
209];
210
211const PROJECT_LANGUAGE_SPECS: &[(LanguageId, &[&str])] = &[
212 (LanguageId::Rust, &["Cargo.toml"]),
213 (LanguageId::TypeScript, &["package.json"]),
214 (LanguageId::Python, &["pyproject.toml", "setup.py", "requirements.txt"]),
215 (LanguageId::Go, &["go.mod"]),
216 (LanguageId::Cpp, &["CMakeLists.txt"]),
217];
218
219const LANGUAGE_SPECS: &[LanguageSpec] = &[
220 LanguageSpec {
221 metadata: LanguageMetadata {
222 id: LanguageId::Rust,
223 primary_extension: Some("rs"),
224 aliases: &["rust", "rs"],
225 extensions: &["rs"],
226 },
227 server_kind: Some(ServerKind::RustAnalyzer),
228 },
229 LanguageSpec {
230 metadata: LanguageMetadata {
231 id: LanguageId::Python,
232 primary_extension: Some("py"),
233 aliases: &["python", "py"],
234 extensions: &["py", "pyi", "pyw"],
235 },
236 server_kind: Some(ServerKind::Pyright),
237 },
238 LanguageSpec {
239 metadata: LanguageMetadata {
240 id: LanguageId::JavaScript,
241 primary_extension: Some("js"),
242 aliases: &["javascript", "js"],
243 extensions: &["js", "mjs"],
244 },
245 server_kind: Some(ServerKind::TypeScriptLanguageServer),
246 },
247 LanguageSpec {
248 metadata: LanguageMetadata {
249 id: LanguageId::JavaScriptReact,
250 primary_extension: Some("jsx"),
251 aliases: &["javascript", "js", "javascriptreact", "jsx"],
252 extensions: &["jsx"],
253 },
254 server_kind: Some(ServerKind::TypeScriptLanguageServer),
255 },
256 LanguageSpec {
257 metadata: LanguageMetadata {
258 id: LanguageId::TypeScript,
259 primary_extension: Some("ts"),
260 aliases: &["typescript", "ts"],
261 extensions: &["ts"],
262 },
263 server_kind: Some(ServerKind::TypeScriptLanguageServer),
264 },
265 LanguageSpec {
266 metadata: LanguageMetadata {
267 id: LanguageId::TypeScriptReact,
268 primary_extension: Some("tsx"),
269 aliases: &["typescript", "ts", "typescriptreact", "tsx"],
270 extensions: &["tsx"],
271 },
272 server_kind: Some(ServerKind::TypeScriptLanguageServer),
273 },
274 LanguageSpec {
275 metadata: LanguageMetadata {
276 id: LanguageId::Go,
277 primary_extension: Some("go"),
278 aliases: &["go"],
279 extensions: &["go"],
280 },
281 server_kind: Some(ServerKind::Gopls),
282 },
283 LanguageSpec {
284 metadata: LanguageMetadata {
285 id: LanguageId::Java,
286 primary_extension: Some("java"),
287 aliases: &["java"],
288 extensions: &["java"],
289 },
290 server_kind: None,
291 },
292 LanguageSpec {
293 metadata: LanguageMetadata {
294 id: LanguageId::C,
295 primary_extension: Some("c"),
296 aliases: &["c"],
297 extensions: &["c", "h"],
298 },
299 server_kind: Some(ServerKind::Clangd),
300 },
301 LanguageSpec {
302 metadata: LanguageMetadata {
303 id: LanguageId::Cpp,
304 primary_extension: Some("cpp"),
305 aliases: &["cpp", "c++"],
306 extensions: &["cpp", "cxx", "cc", "hpp", "hxx", "hh"],
307 },
308 server_kind: Some(ServerKind::Clangd),
309 },
310 LanguageSpec {
311 metadata: LanguageMetadata {
312 id: LanguageId::CSharp,
313 primary_extension: Some("cs"),
314 aliases: &["csharp", "cs"],
315 extensions: &["cs"],
316 },
317 server_kind: None,
318 },
319 LanguageSpec {
320 metadata: LanguageMetadata {
321 id: LanguageId::Ruby,
322 primary_extension: Some("rb"),
323 aliases: &["ruby", "rb"],
324 extensions: &["rb"],
325 },
326 server_kind: None,
327 },
328 LanguageSpec {
329 metadata: LanguageMetadata {
330 id: LanguageId::Php,
331 primary_extension: Some("php"),
332 aliases: &["php"],
333 extensions: &["php"],
334 },
335 server_kind: None,
336 },
337 LanguageSpec {
338 metadata: LanguageMetadata {
339 id: LanguageId::Swift,
340 primary_extension: Some("swift"),
341 aliases: &["swift"],
342 extensions: &["swift"],
343 },
344 server_kind: None,
345 },
346 LanguageSpec {
347 metadata: LanguageMetadata {
348 id: LanguageId::Kotlin,
349 primary_extension: Some("kt"),
350 aliases: &["kotlin"],
351 extensions: &["kt", "kts"],
352 },
353 server_kind: None,
354 },
355 LanguageSpec {
356 metadata: LanguageMetadata {
357 id: LanguageId::Scala,
358 primary_extension: Some("scala"),
359 aliases: &["scala"],
360 extensions: &["scala"],
361 },
362 server_kind: None,
363 },
364 LanguageSpec {
365 metadata: LanguageMetadata {
366 id: LanguageId::Html,
367 primary_extension: Some("html"),
368 aliases: &["html"],
369 extensions: &["html", "htm"],
370 },
371 server_kind: None,
372 },
373 LanguageSpec {
374 metadata: LanguageMetadata {
375 id: LanguageId::Css,
376 primary_extension: Some("css"),
377 aliases: &["css"],
378 extensions: &["css"],
379 },
380 server_kind: None,
381 },
382 LanguageSpec {
383 metadata: LanguageMetadata {
384 id: LanguageId::Json,
385 primary_extension: Some("json"),
386 aliases: &["json"],
387 extensions: &["json"],
388 },
389 server_kind: None,
390 },
391 LanguageSpec {
392 metadata: LanguageMetadata {
393 id: LanguageId::Yaml,
394 primary_extension: Some("yaml"),
395 aliases: &["yaml", "yml"],
396 extensions: &["yaml", "yml"],
397 },
398 server_kind: None,
399 },
400 LanguageSpec {
401 metadata: LanguageMetadata {
402 id: LanguageId::Toml,
403 primary_extension: Some("toml"),
404 aliases: &["toml"],
405 extensions: &["toml"],
406 },
407 server_kind: None,
408 },
409 LanguageSpec {
410 metadata: LanguageMetadata {
411 id: LanguageId::Markdown,
412 primary_extension: Some("md"),
413 aliases: &["markdown", "md"],
414 extensions: &["md", "markdown"],
415 },
416 server_kind: None,
417 },
418 LanguageSpec {
419 metadata: LanguageMetadata {
420 id: LanguageId::Xml,
421 primary_extension: Some("xml"),
422 aliases: &["xml"],
423 extensions: &["xml"],
424 },
425 server_kind: None,
426 },
427 LanguageSpec {
428 metadata: LanguageMetadata {
429 id: LanguageId::Sql,
430 primary_extension: Some("sql"),
431 aliases: &["sql"],
432 extensions: &["sql"],
433 },
434 server_kind: None,
435 },
436 LanguageSpec {
437 metadata: LanguageMetadata {
438 id: LanguageId::ShellScript,
439 primary_extension: Some("sh"),
440 aliases: &["sh", "shell", "bash"],
441 extensions: &["sh", "bash", "zsh"],
442 },
443 server_kind: None,
444 },
445 LanguageSpec {
446 metadata: LanguageMetadata {
447 id: LanguageId::PlainText,
448 primary_extension: None,
449 aliases: &["plaintext", "text", "txt"],
450 extensions: &["txt"],
451 },
452 server_kind: None,
453 },
454];
455
456pub static LANGUAGE_METADATA: LazyLock<Vec<LanguageMetadata>> =
457 LazyLock::new(|| LANGUAGE_SPECS.iter().map(|spec| spec.metadata).collect());
458
459static CONFIG_MAP: LazyLock<HashMap<LanguageId, LspConfig>> = LazyLock::new(|| {
460 let languages_by_server: HashMap<ServerKind, Vec<LanguageId>> = LANGUAGE_SPECS
461 .iter()
462 .filter_map(|spec| spec.server_kind.map(|kind| (kind, spec.metadata.id)))
463 .fold(HashMap::new(), |mut acc, (kind, id)| {
464 acc.entry(kind).or_default().push(id);
465 acc
466 });
467
468 LANGUAGE_SPECS
469 .iter()
470 .filter_map(|spec| {
471 let server_kind = spec.server_kind?;
472 let server = SERVER_SPECS.iter().find(|server| server.kind == server_kind)?;
473 Some((
474 spec.metadata.id,
475 LspConfig::new(server.command)
476 .with_args(server.args.iter().map(|arg| (*arg).to_string()).collect())
477 .with_languages(languages_by_server.get(&server_kind).cloned().unwrap_or_default()),
478 ))
479 })
480 .collect()
481});
482
483pub(crate) fn server_kind_for_language(id: LanguageId) -> Option<ServerKind> {
484 LANGUAGE_SPECS.iter().find(|spec| spec.metadata.id == id).and_then(|spec| spec.server_kind)
485}
486
487pub(crate) fn socket_identity_for_language(id: LanguageId) -> &'static str {
488 server_kind_for_language(id).map_or_else(|| id.as_str(), ServerKind::as_str)
489}
490
491pub(crate) fn resolved_config_for_language(language: LanguageId) -> Option<LspConfig> {
492 let mut config = get_config_for_language(language)?.clone();
493 let server_kind = server_kind_for_language(language)?;
494
495 let command_key = format!("AETHER_LSPD_SERVER_COMMAND_{}", server_kind.env_key());
496 if let Some(command) = std::env::var_os(command_key) {
497 config.command = command.to_string_lossy().into_owned();
498 }
499
500 let args_key = format!("AETHER_LSPD_SERVER_ARGS_{}", server_kind.env_key());
501 if let Ok(args) = std::env::var(args_key)
502 && let Ok(parsed) = serde_json::from_str::<Vec<String>>(&args)
503 {
504 config.args = parsed;
505 }
506
507 Some(config)
508}
509
510pub(crate) fn from_extension(ext: &str) -> Option<LanguageId> {
511 LANGUAGE_SPECS.iter().find(|spec| spec.metadata.extensions.contains(&ext)).map(|spec| spec.metadata.id)
512}
513
514pub fn metadata_for(id: LanguageId) -> Option<&'static LanguageMetadata> {
515 LANGUAGE_METADATA.iter().find(|metadata| metadata.id == id)
516}
517
518pub fn from_lsp_id(lsp_id: &str) -> Option<LanguageId> {
519 LANGUAGE_SPECS.iter().find(|spec| spec.metadata.id.as_str() == lsp_id).map(|spec| spec.metadata.id)
520}
521
522pub fn extensions_for_alias(alias: &str) -> Vec<&'static str> {
523 let lower = alias.to_lowercase();
524 LANGUAGE_SPECS
525 .iter()
526 .filter(|spec| spec.metadata.aliases.iter().any(|candidate| *candidate == lower))
527 .flat_map(|spec| spec.metadata.extensions.iter().copied())
528 .collect()
529}
530
531pub fn detect_project_languages(root: &Path) -> Vec<LanguageId> {
532 PROJECT_LANGUAGE_SPECS
533 .iter()
534 .filter(|(_, manifests)| manifests.iter().any(|manifest| root.join(manifest).is_file()))
535 .map(|(language, _)| *language)
536 .collect()
537}
538
539pub fn server_metadata_for_language(language: LanguageId) -> Option<LanguageServerMetadata> {
540 let server_kind = server_kind_for_language(language)?;
541 let server = SERVER_SPECS.iter().find(|server| server.kind == server_kind)?;
542 Some(LanguageServerMetadata {
543 display_name: server.display_name,
544 installation_instructions: server.installation_instructions,
545 })
546}
547
548pub fn get_config_for_language(language: LanguageId) -> Option<&'static LspConfig> {
549 CONFIG_MAP.get(&language)
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555
556 #[test]
557 fn detects_project_languages_from_manifests() {
558 let root = tempfile::tempdir().unwrap();
559 std::fs::write(root.path().join("Cargo.toml"), "").unwrap();
560 std::fs::write(root.path().join("package.json"), "{}").unwrap();
561
562 assert_eq!(detect_project_languages(root.path()), vec![LanguageId::Rust, LanguageId::TypeScript]);
563 }
564
565 #[test]
566 fn typescript_server_metadata_has_installation_instructions() {
567 let metadata = server_metadata_for_language(LanguageId::TypeScript).unwrap();
568
569 assert_eq!(metadata.display_name, "TypeScript language server");
570 assert!(metadata.installation_instructions.unwrap().contains("npm install --save-dev"));
571 }
572
573 #[test]
574 fn typescript_family_shares_server_kind() {
575 assert_eq!(
576 server_kind_for_language(LanguageId::TypeScript),
577 server_kind_for_language(LanguageId::TypeScriptReact)
578 );
579 assert_eq!(
580 socket_identity_for_language(LanguageId::TypeScript),
581 socket_identity_for_language(LanguageId::TypeScriptReact)
582 );
583 }
584
585 #[test]
586 fn c_family_shares_server_kind() {
587 assert_eq!(server_kind_for_language(LanguageId::C), server_kind_for_language(LanguageId::Cpp));
588 assert_eq!(socket_identity_for_language(LanguageId::C), socket_identity_for_language(LanguageId::Cpp));
589 }
590
591 #[test]
592 fn metadata_for_returns_correct_data() {
593 let meta = metadata_for(LanguageId::Rust).unwrap();
594 assert_eq!(meta.id.as_str(), "rust");
595 assert_eq!(meta.primary_extension, Some("rs"));
596 assert!(meta.aliases.contains(&"rust"));
597 assert!(meta.aliases.contains(&"rs"));
598 }
599
600 #[test]
601 fn from_lsp_id_resolves_known_languages() {
602 assert_eq!(from_lsp_id("rust"), Some(LanguageId::Rust));
603 assert_eq!(from_lsp_id("typescriptreact"), Some(LanguageId::TypeScriptReact));
604 assert_eq!(from_lsp_id("unknown"), None);
605 }
606
607 #[test]
608 fn primary_extension_delegates_to_catalog() {
609 assert_eq!(LanguageId::Rust.primary_extension(), Some("rs"));
610 assert_eq!(LanguageId::Python.primary_extension(), Some("py"));
611 assert_eq!(LanguageId::PlainText.primary_extension(), None);
612 }
613
614 #[test]
615 fn extensions_for_alias_includes_related_variants() {
616 let js_exts = extensions_for_alias("javascript");
617 assert!(js_exts.contains(&"js"));
618 assert!(js_exts.contains(&"mjs"));
619 assert!(js_exts.contains(&"jsx"));
620
621 let ts_exts = extensions_for_alias("typescript");
622 assert!(ts_exts.contains(&"ts"));
623 assert!(ts_exts.contains(&"tsx"));
624
625 let sh_exts = extensions_for_alias("bash");
626 assert!(sh_exts.contains(&"sh"));
627 assert!(sh_exts.contains(&"bash"));
628 assert!(sh_exts.contains(&"zsh"));
629 }
630
631 #[test]
632 fn all_languages_have_metadata() {
633 let variants = [
634 LanguageId::Rust,
635 LanguageId::Python,
636 LanguageId::JavaScript,
637 LanguageId::JavaScriptReact,
638 LanguageId::TypeScript,
639 LanguageId::TypeScriptReact,
640 LanguageId::Go,
641 LanguageId::Java,
642 LanguageId::C,
643 LanguageId::Cpp,
644 LanguageId::CSharp,
645 LanguageId::Ruby,
646 LanguageId::Php,
647 LanguageId::Swift,
648 LanguageId::Kotlin,
649 LanguageId::Scala,
650 LanguageId::Html,
651 LanguageId::Css,
652 LanguageId::Json,
653 LanguageId::Yaml,
654 LanguageId::Toml,
655 LanguageId::Markdown,
656 LanguageId::Xml,
657 LanguageId::Sql,
658 LanguageId::ShellScript,
659 LanguageId::PlainText,
660 ];
661
662 for variant in variants {
663 assert!(metadata_for(variant).is_some(), "Missing metadata for {variant:?}");
664 }
665 }
666
667 #[test]
668 fn language_id_as_str() {
669 assert_eq!(LanguageId::Rust.as_str(), "rust");
670 assert_eq!(LanguageId::TypeScriptReact.as_str(), "typescriptreact");
671 }
672
673 #[test]
674 fn language_id_from_extension() {
675 assert_eq!(LanguageId::from_extension("rs"), Some(LanguageId::Rust));
676 assert_eq!(LanguageId::from_extension("tsx"), Some(LanguageId::TypeScriptReact));
677 assert_eq!(LanguageId::from_extension("xyz"), None);
678 }
679
680 #[test]
681 fn language_id_from_path() {
682 assert_eq!(LanguageId::from_path(Path::new("foo.rs")), LanguageId::Rust);
683 assert_eq!(LanguageId::from_path(Path::new("bar.py")), LanguageId::Python);
684 assert_eq!(LanguageId::from_path(Path::new("baz.tsx")), LanguageId::TypeScriptReact);
685 assert_eq!(LanguageId::from_path(Path::new("unknown.xyz")), LanguageId::PlainText);
686 assert_eq!(LanguageId::from_path(Path::new("no_extension")), LanguageId::PlainText);
687 }
688
689 #[test]
690 fn lsp_config_builder() {
691 let config = LspConfig::new("test-lsp")
692 .with_args(vec!["--stdio".to_string(), "--debug".to_string()])
693 .with_languages(vec![LanguageId::Rust]);
694
695 assert_eq!(config.command, "test-lsp");
696 assert_eq!(config.args, vec!["--stdio", "--debug"]);
697 assert_eq!(config.languages, vec![LanguageId::Rust]);
698 }
699
700 #[test]
701 fn get_config_for_known_languages() {
702 let rust_config = get_config_for_language(LanguageId::Rust);
703 assert!(rust_config.is_some());
704 assert_eq!(rust_config.unwrap().command, "rust-analyzer");
705
706 let ts_config = get_config_for_language(LanguageId::TypeScript);
707 assert!(ts_config.is_some());
708 assert_eq!(ts_config.unwrap().command, "typescript-language-server");
709
710 let plaintext_config = get_config_for_language(LanguageId::PlainText);
711 assert!(plaintext_config.is_none());
712 }
713}