Skip to main content

aft/lsp/
registry.rs

1use std::path::Path;
2
3/// Unique identifier for a language server kind.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum ServerKind {
6    TypeScript,
7    Python,
8    Rust,
9    Go,
10}
11
12/// Definition of a language server.
13#[derive(Debug, PartialEq, Eq)]
14pub struct ServerDef {
15    pub kind: ServerKind,
16    /// Display name.
17    pub name: &'static str,
18    /// File extensions this server handles.
19    pub extensions: &'static [&'static str],
20    /// Binary name to look up on PATH.
21    pub binary: &'static str,
22    /// Arguments to pass when spawning.
23    pub args: &'static [&'static str],
24    /// Root marker files — presence indicates a workspace root.
25    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
69/// Built-in server definitions.
70pub fn builtin_servers() -> &'static [ServerDef] {
71    BUILTIN_SERVERS
72}
73
74impl ServerDef {
75    /// Check if this server handles a given file extension.
76    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    /// Check if the server binary is available on PATH.
83    pub fn is_available(&self) -> bool {
84        which::which(self.binary).is_ok()
85    }
86}
87
88/// Find all server definitions that handle a given file path.
89pub 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}