1use std::collections::HashMap;
2use std::path::Path;
3use std::sync::Arc;
4
5use crate::config::{Config, UserServerDef};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub enum ServerKind {
10 TypeScript,
11 Python,
12 Rust,
13 Go,
14 Bash,
15 Yaml,
16 Ty,
17 Custom(Arc<str>),
18}
19
20impl ServerKind {
21 pub fn id_str(&self) -> &str {
22 match self {
23 Self::TypeScript => "typescript",
24 Self::Python => "python",
25 Self::Rust => "rust",
26 Self::Go => "go",
27 Self::Bash => "bash",
28 Self::Yaml => "yaml",
29 Self::Ty => "ty",
30 Self::Custom(id) => id.as_ref(),
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ServerDef {
38 pub kind: ServerKind,
39 pub name: String,
41 pub extensions: Vec<String>,
43 pub binary: String,
45 pub args: Vec<String>,
47 pub root_markers: Vec<String>,
49 pub env: HashMap<String, String>,
51 pub initialization_options: Option<serde_json::Value>,
53}
54
55impl ServerDef {
56 pub fn matches_extension(&self, ext: &str) -> bool {
58 self.extensions
59 .iter()
60 .any(|candidate| candidate.eq_ignore_ascii_case(ext))
61 }
62
63 pub fn is_available(&self) -> bool {
65 which::which(&self.binary).is_ok()
66 }
67}
68
69pub fn builtin_servers() -> Vec<ServerDef> {
71 vec![
72 builtin_server(
73 ServerKind::TypeScript,
74 "TypeScript Language Server",
75 &["ts", "tsx", "js", "jsx", "mjs", "cjs"],
76 "typescript-language-server",
77 &["--stdio"],
78 &["tsconfig.json", "jsconfig.json", "package.json"],
79 ),
80 builtin_server(
81 ServerKind::Python,
82 "Pyright",
83 &["py", "pyi"],
84 "pyright-langserver",
85 &["--stdio"],
86 &[
87 "pyproject.toml",
88 "setup.py",
89 "setup.cfg",
90 "pyrightconfig.json",
91 "requirements.txt",
92 ],
93 ),
94 builtin_server(
95 ServerKind::Rust,
96 "rust-analyzer",
97 &["rs"],
98 "rust-analyzer",
99 &[],
100 &["Cargo.toml"],
101 ),
102 builtin_server_with_init(
107 ServerKind::Go,
108 "gopls",
109 &["go"],
110 "gopls",
111 &["serve"],
112 &["go.mod"],
113 serde_json::json!({ "pullDiagnostics": true }),
114 ),
115 builtin_server(
116 ServerKind::Bash,
117 "bash-language-server",
118 &["sh", "bash", "zsh"],
119 "bash-language-server",
120 &["start"],
121 &["package.json", ".git"],
122 ),
123 builtin_server(
124 ServerKind::Yaml,
125 "yaml-language-server",
126 &["yaml", "yml"],
127 "yaml-language-server",
128 &["--stdio"],
129 &["package.json", ".git"],
130 ),
131 builtin_server(
132 ServerKind::Ty,
133 "ty",
134 &["py", "pyi"],
135 "ty",
136 &["server"],
137 &[
138 "pyproject.toml",
139 "ty.toml",
140 "setup.py",
141 "setup.cfg",
142 "requirements.txt",
143 "Pipfile",
144 "pyrightconfig.json",
145 ],
146 ),
147 ]
148}
149
150pub fn servers_for_file(path: &Path, config: &Config) -> Vec<ServerDef> {
152 let extension = path
153 .extension()
154 .and_then(|ext| ext.to_str())
155 .unwrap_or_default();
156
157 builtin_servers()
158 .into_iter()
159 .chain(config.lsp_servers.iter().filter_map(custom_server))
160 .filter(|server| !is_disabled(server, config))
161 .filter(|server| config.experimental_lsp_ty || server.kind != ServerKind::Ty)
162 .filter(|server| server.matches_extension(extension))
163 .collect()
164}
165
166fn builtin_server(
167 kind: ServerKind,
168 name: &str,
169 extensions: &[&str],
170 binary: &str,
171 args: &[&str],
172 root_markers: &[&str],
173) -> ServerDef {
174 ServerDef {
175 kind,
176 name: name.to_string(),
177 extensions: strings(extensions),
178 binary: binary.to_string(),
179 args: strings(args),
180 root_markers: strings(root_markers),
181 env: HashMap::new(),
182 initialization_options: None,
183 }
184}
185
186fn builtin_server_with_init(
190 kind: ServerKind,
191 name: &str,
192 extensions: &[&str],
193 binary: &str,
194 args: &[&str],
195 root_markers: &[&str],
196 initialization_options: serde_json::Value,
197) -> ServerDef {
198 let mut def = builtin_server(kind, name, extensions, binary, args, root_markers);
199 def.initialization_options = Some(initialization_options);
200 def
201}
202
203fn custom_server(server: &UserServerDef) -> Option<ServerDef> {
204 if server.disabled {
205 return None;
206 }
207
208 Some(ServerDef {
209 kind: ServerKind::Custom(Arc::from(server.id.as_str())),
210 name: server.id.clone(),
211 extensions: server.extensions.clone(),
212 binary: server.binary.clone(),
213 args: server.args.clone(),
214 root_markers: server.root_markers.clone(),
215 env: server.env.clone(),
216 initialization_options: server.initialization_options.clone(),
217 })
218}
219
220fn is_disabled(server: &ServerDef, config: &Config) -> bool {
221 config
222 .disabled_lsp
223 .contains(&server.kind.id_str().to_ascii_lowercase())
224}
225
226fn strings(values: &[&str]) -> Vec<String> {
227 values.iter().map(|value| (*value).to_string()).collect()
228}
229
230#[cfg(test)]
231mod tests {
232 use std::path::Path;
233 use std::sync::Arc;
234
235 use crate::config::{Config, UserServerDef};
236
237 use super::{servers_for_file, ServerKind};
238
239 fn matching_kinds(path: &str, config: &Config) -> Vec<ServerKind> {
240 servers_for_file(Path::new(path), config)
241 .into_iter()
242 .map(|server| server.kind)
243 .collect()
244 }
245
246 #[test]
247 fn test_servers_for_typescript_file() {
248 assert_eq!(
249 matching_kinds("/tmp/file.ts", &Config::default()),
250 vec![ServerKind::TypeScript]
251 );
252 }
253
254 #[test]
255 fn test_servers_for_python_file() {
256 assert_eq!(
257 matching_kinds("/tmp/file.py", &Config::default()),
258 vec![ServerKind::Python]
259 );
260 }
261
262 #[test]
263 fn test_servers_for_rust_file() {
264 assert_eq!(
265 matching_kinds("/tmp/file.rs", &Config::default()),
266 vec![ServerKind::Rust]
267 );
268 }
269
270 #[test]
271 fn test_servers_for_go_file() {
272 assert_eq!(
273 matching_kinds("/tmp/file.go", &Config::default()),
274 vec![ServerKind::Go]
275 );
276 }
277
278 #[test]
279 fn test_servers_for_unknown_file() {
280 assert!(matching_kinds("/tmp/file.txt", &Config::default()).is_empty());
281 }
282
283 #[test]
284 fn test_tsx_matches_typescript() {
285 assert_eq!(
286 matching_kinds("/tmp/file.tsx", &Config::default()),
287 vec![ServerKind::TypeScript]
288 );
289 }
290
291 #[test]
292 fn test_case_insensitive_extension() {
293 assert_eq!(
294 matching_kinds("/tmp/file.TS", &Config::default()),
295 vec![ServerKind::TypeScript]
296 );
297 }
298
299 #[test]
300 fn test_bash_and_yaml_builtins() {
301 assert_eq!(
302 matching_kinds("/tmp/file.sh", &Config::default()),
303 vec![ServerKind::Bash]
304 );
305 assert_eq!(
306 matching_kinds("/tmp/file.yaml", &Config::default()),
307 vec![ServerKind::Yaml]
308 );
309 }
310
311 #[test]
312 fn test_ty_requires_experimental_flag() {
313 assert_eq!(
314 matching_kinds("/tmp/file.py", &Config::default()),
315 vec![ServerKind::Python]
316 );
317
318 let config = Config {
319 experimental_lsp_ty: true,
320 ..Config::default()
321 };
322 assert_eq!(
323 matching_kinds("/tmp/file.py", &config),
324 vec![ServerKind::Python, ServerKind::Ty]
325 );
326 }
327
328 #[test]
329 fn test_custom_server_matches_extension() {
330 let config = Config {
331 lsp_servers: vec![UserServerDef {
332 id: "tinymist".to_string(),
333 extensions: vec!["typ".to_string()],
334 binary: "tinymist".to_string(),
335 root_markers: vec!["typst.toml".to_string()],
336 ..UserServerDef::default()
337 }],
338 ..Config::default()
339 };
340
341 assert_eq!(
342 matching_kinds("/tmp/file.typ", &config),
343 vec![ServerKind::Custom(Arc::from("tinymist"))]
344 );
345 }
346}