Skip to main content

atomcode_core/lsp/
registry.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct LspServerConfig {
7    pub command: String,
8    #[serde(default)]
9    pub args: Vec<String>,
10    #[serde(default)]
11    pub root_markers: Vec<String>,
12}
13
14pub struct LspServerRegistry {
15    servers: HashMap<String, LspServerConfig>,
16}
17
18impl LspServerRegistry {
19    pub fn empty() -> Self {
20        Self {
21            servers: HashMap::new(),
22        }
23    }
24
25    pub fn with_defaults() -> Self {
26        let mut servers = HashMap::new();
27        servers.insert(
28            "rs".into(),
29            LspServerConfig {
30                command: "rust-analyzer".into(),
31                args: vec![],
32                root_markers: vec!["Cargo.toml".into()],
33            },
34        );
35        servers.insert(
36            "ts".into(),
37            LspServerConfig {
38                command: "typescript-language-server".into(),
39                args: vec!["--stdio".into()],
40                root_markers: vec!["tsconfig.json".into(), "package.json".into()],
41            },
42        );
43        servers.insert(
44            "tsx".into(),
45            LspServerConfig {
46                command: "typescript-language-server".into(),
47                args: vec!["--stdio".into()],
48                root_markers: vec!["tsconfig.json".into()],
49            },
50        );
51        servers.insert(
52            "js".into(),
53            LspServerConfig {
54                command: "typescript-language-server".into(),
55                args: vec!["--stdio".into()],
56                root_markers: vec!["package.json".into()],
57            },
58        );
59        servers.insert(
60            "py".into(),
61            LspServerConfig {
62                command: "pylsp".into(),
63                args: vec![],
64                root_markers: vec!["pyproject.toml".into(), "setup.py".into()],
65            },
66        );
67        servers.insert(
68            "go".into(),
69            LspServerConfig {
70                command: "gopls".into(),
71                args: vec!["serve".into()],
72                root_markers: vec!["go.mod".into()],
73            },
74        );
75        servers.insert(
76            "java".into(),
77            LspServerConfig {
78                command: "jdtls".into(),
79                args: vec![],
80                root_markers: vec!["pom.xml".into(), "build.gradle".into()],
81            },
82        );
83        Self { servers }
84    }
85
86    pub fn get(&self, extension: &str) -> Option<&LspServerConfig> {
87        self.servers.get(extension)
88    }
89
90    pub fn server_for_file(&self, file_path: &str) -> Option<&LspServerConfig> {
91        let ext = file_path.rsplit('.').next()?;
92        self.servers.get(ext)
93    }
94
95    pub fn merge_user_config(&mut self, user: HashMap<String, LspServerConfig>) {
96        for (ext, config) in user {
97            self.servers.insert(ext, config);
98        }
99    }
100
101    pub fn available_languages(&self) -> Vec<&str> {
102        let mut langs: Vec<_> = self.servers.keys().map(String::as_str).collect();
103        langs.sort();
104        langs
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn defaults_include_common_langs() {
114        let registry = LspServerRegistry::with_defaults();
115        let langs = registry.available_languages();
116        assert!(langs.contains(&"rs"));
117        assert!(langs.contains(&"ts"));
118        assert!(langs.contains(&"py"));
119        assert!(langs.contains(&"go"));
120        assert!(langs.contains(&"java"));
121        assert!(langs.contains(&"js"));
122        assert!(langs.contains(&"tsx"));
123    }
124
125    #[test]
126    fn server_for_file_resolves() {
127        let registry = LspServerRegistry::with_defaults();
128
129        let rust_cfg = registry.server_for_file("src/main.rs").unwrap();
130        assert_eq!(rust_cfg.command, "rust-analyzer");
131
132        let ts_cfg = registry.server_for_file("app/index.ts").unwrap();
133        assert_eq!(ts_cfg.command, "typescript-language-server");
134
135        let py_cfg = registry.server_for_file("script.py").unwrap();
136        assert_eq!(py_cfg.command, "pylsp");
137
138        // Unknown extension returns None
139        assert!(registry.server_for_file("data.csv").is_none());
140    }
141
142    #[test]
143    fn user_config_overrides() {
144        let mut registry = LspServerRegistry::with_defaults();
145
146        // Override Rust to use a custom analyzer
147        let mut user = HashMap::new();
148        user.insert(
149            "rs".into(),
150            LspServerConfig {
151                command: "custom-rust-analyzer".into(),
152                args: vec!["--custom".into()],
153                root_markers: vec!["Cargo.toml".into()],
154            },
155        );
156        // Add a new language
157        user.insert(
158            "rb".into(),
159            LspServerConfig {
160                command: "solargraph".into(),
161                args: vec!["stdio".into()],
162                root_markers: vec!["Gemfile".into()],
163            },
164        );
165        registry.merge_user_config(user);
166
167        let rust_cfg = registry.get("rs").unwrap();
168        assert_eq!(rust_cfg.command, "custom-rust-analyzer");
169
170        let ruby_cfg = registry.get("rb").unwrap();
171        assert_eq!(ruby_cfg.command, "solargraph");
172    }
173}