1use std::path::Path;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum ServerKind {
6 TypeScript,
7 Python,
8 Rust,
9 Go,
10}
11
12#[derive(Debug, PartialEq, Eq)]
14pub struct ServerDef {
15 pub kind: ServerKind,
16 pub name: &'static str,
18 pub extensions: &'static [&'static str],
20 pub binary: &'static str,
22 pub args: &'static [&'static str],
24 pub root_markers: &'static [&'static str],
26}
27
28const BUILTIN_SERVERS: &[ServerDef] = &[
29 ServerDef {
30 kind: ServerKind::TypeScript,
31 name: "TypeScript Language Server",
32 extensions: &["ts", "tsx", "js", "jsx", "mjs", "cjs"],
33 binary: "typescript-language-server",
34 args: &["--stdio"],
35 root_markers: &["tsconfig.json", "jsconfig.json", "package.json"],
36 },
37 ServerDef {
38 kind: ServerKind::Python,
39 name: "Pyright",
40 extensions: &["py", "pyi"],
41 binary: "pyright-langserver",
42 args: &["--stdio"],
43 root_markers: &[
44 "pyproject.toml",
45 "setup.py",
46 "setup.cfg",
47 "pyrightconfig.json",
48 "requirements.txt",
49 ],
50 },
51 ServerDef {
52 kind: ServerKind::Rust,
53 name: "rust-analyzer",
54 extensions: &["rs"],
55 binary: "rust-analyzer",
56 args: &[],
57 root_markers: &["Cargo.toml"],
58 },
59 ServerDef {
60 kind: ServerKind::Go,
61 name: "gopls",
62 extensions: &["go"],
63 binary: "gopls",
64 args: &["serve"],
65 root_markers: &["go.mod"],
66 },
67];
68
69pub fn builtin_servers() -> &'static [ServerDef] {
71 BUILTIN_SERVERS
72}
73
74impl ServerDef {
75 pub fn matches_extension(&self, ext: &str) -> bool {
77 self.extensions
78 .iter()
79 .any(|candidate| candidate.eq_ignore_ascii_case(ext))
80 }
81
82 pub fn is_available(&self) -> bool {
84 which::which(self.binary).is_ok()
85 }
86}
87
88pub fn servers_for_file(path: &Path) -> Vec<&'static ServerDef> {
90 let extension = path
91 .extension()
92 .and_then(|ext| ext.to_str())
93 .unwrap_or_default();
94
95 builtin_servers()
96 .iter()
97 .filter(|server| server.matches_extension(extension))
98 .collect()
99}
100
101#[cfg(test)]
102mod tests {
103 use std::path::Path;
104
105 use super::{servers_for_file, ServerKind};
106
107 fn matching_kinds(path: &str) -> Vec<ServerKind> {
108 servers_for_file(Path::new(path))
109 .into_iter()
110 .map(|server| server.kind)
111 .collect()
112 }
113
114 #[test]
115 fn test_servers_for_typescript_file() {
116 assert_eq!(matching_kinds("/tmp/file.ts"), vec![ServerKind::TypeScript]);
117 }
118
119 #[test]
120 fn test_servers_for_python_file() {
121 assert_eq!(matching_kinds("/tmp/file.py"), vec![ServerKind::Python]);
122 }
123
124 #[test]
125 fn test_servers_for_rust_file() {
126 assert_eq!(matching_kinds("/tmp/file.rs"), vec![ServerKind::Rust]);
127 }
128
129 #[test]
130 fn test_servers_for_go_file() {
131 assert_eq!(matching_kinds("/tmp/file.go"), vec![ServerKind::Go]);
132 }
133
134 #[test]
135 fn test_servers_for_unknown_file() {
136 assert!(matching_kinds("/tmp/file.txt").is_empty());
137 }
138
139 #[test]
140 fn test_tsx_matches_typescript() {
141 assert_eq!(
142 matching_kinds("/tmp/file.tsx"),
143 vec![ServerKind::TypeScript]
144 );
145 }
146
147 #[test]
148 fn test_case_insensitive_extension() {
149 assert_eq!(matching_kinds("/tmp/file.TS"), vec![ServerKind::TypeScript]);
150 }
151}