mcpls_core/config/
server.rs1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct LspServerConfig {
11 pub language_id: String,
13
14 pub command: String,
16
17 #[serde(default)]
19 pub args: Vec<String>,
20
21 #[serde(default)]
23 pub env: HashMap<String, String>,
24
25 #[serde(default)]
27 pub file_patterns: Vec<String>,
28
29 #[serde(default)]
31 pub initialization_options: Option<serde_json::Value>,
32
33 #[serde(default = "default_timeout")]
35 pub timeout_seconds: u64,
36}
37
38const fn default_timeout() -> u64 {
39 30
40}
41
42impl LspServerConfig {
43 #[must_use]
45 pub fn rust_analyzer() -> Self {
46 Self {
47 language_id: "rust".to_string(),
48 command: "rust-analyzer".to_string(),
49 args: vec![],
50 env: HashMap::new(),
51 file_patterns: vec!["**/*.rs".to_string()],
52 initialization_options: None,
53 timeout_seconds: default_timeout(),
54 }
55 }
56
57 #[must_use]
59 pub fn pyright() -> Self {
60 Self {
61 language_id: "python".to_string(),
62 command: "pyright-langserver".to_string(),
63 args: vec!["--stdio".to_string()],
64 env: HashMap::new(),
65 file_patterns: vec!["**/*.py".to_string()],
66 initialization_options: None,
67 timeout_seconds: default_timeout(),
68 }
69 }
70
71 #[must_use]
73 pub fn typescript() -> Self {
74 Self {
75 language_id: "typescript".to_string(),
76 command: "typescript-language-server".to_string(),
77 args: vec!["--stdio".to_string()],
78 env: HashMap::new(),
79 file_patterns: vec!["**/*.ts".to_string(), "**/*.tsx".to_string()],
80 initialization_options: None,
81 timeout_seconds: default_timeout(),
82 }
83 }
84}
85
86#[cfg(test)]
87#[allow(clippy::unwrap_used)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn test_rust_analyzer_defaults() {
93 let config = LspServerConfig::rust_analyzer();
94
95 assert_eq!(config.language_id, "rust");
96 assert_eq!(config.command, "rust-analyzer");
97 assert!(config.args.is_empty());
98 assert!(config.env.is_empty());
99 assert_eq!(config.file_patterns, vec!["**/*.rs"]);
100 assert!(config.initialization_options.is_none());
101 assert_eq!(config.timeout_seconds, 30);
102 }
103
104 #[test]
105 fn test_pyright_defaults() {
106 let config = LspServerConfig::pyright();
107
108 assert_eq!(config.language_id, "python");
109 assert_eq!(config.command, "pyright-langserver");
110 assert_eq!(config.args, vec!["--stdio"]);
111 assert!(config.env.is_empty());
112 assert_eq!(config.file_patterns, vec!["**/*.py"]);
113 assert!(config.initialization_options.is_none());
114 assert_eq!(config.timeout_seconds, 30);
115 }
116
117 #[test]
118 fn test_typescript_defaults() {
119 let config = LspServerConfig::typescript();
120
121 assert_eq!(config.language_id, "typescript");
122 assert_eq!(config.command, "typescript-language-server");
123 assert_eq!(config.args, vec!["--stdio"]);
124 assert!(config.env.is_empty());
125 assert_eq!(config.file_patterns, vec!["**/*.ts", "**/*.tsx"]);
126 assert!(config.initialization_options.is_none());
127 assert_eq!(config.timeout_seconds, 30);
128 }
129
130 #[test]
131 fn test_default_timeout() {
132 assert_eq!(default_timeout(), 30);
133 }
134
135 #[test]
136 fn test_custom_config() {
137 let mut env = HashMap::new();
138 env.insert("RUST_LOG".to_string(), "debug".to_string());
139
140 let config = LspServerConfig {
141 language_id: "custom".to_string(),
142 command: "custom-lsp".to_string(),
143 args: vec!["--flag".to_string()],
144 env: env.clone(),
145 file_patterns: vec!["**/*.custom".to_string()],
146 initialization_options: Some(serde_json::json!({"key": "value"})),
147 timeout_seconds: 60,
148 };
149
150 assert_eq!(config.language_id, "custom");
151 assert_eq!(config.command, "custom-lsp");
152 assert_eq!(config.args, vec!["--flag"]);
153 assert_eq!(config.env.get("RUST_LOG"), Some(&"debug".to_string()));
154 assert_eq!(config.file_patterns, vec!["**/*.custom"]);
155 assert!(config.initialization_options.is_some());
156 assert_eq!(config.timeout_seconds, 60);
157 }
158
159 #[test]
160 fn test_serde_roundtrip() {
161 let original = LspServerConfig::rust_analyzer();
162
163 let serialized = serde_json::to_string(&original).unwrap();
164 let deserialized: LspServerConfig = serde_json::from_str(&serialized).unwrap();
165
166 assert_eq!(deserialized.language_id, original.language_id);
167 assert_eq!(deserialized.command, original.command);
168 assert_eq!(deserialized.args, original.args);
169 assert_eq!(deserialized.timeout_seconds, original.timeout_seconds);
170 }
171
172 #[test]
173 fn test_clone() {
174 let config = LspServerConfig::rust_analyzer();
175 let cloned = config.clone();
176
177 assert_eq!(cloned.language_id, config.language_id);
178 assert_eq!(cloned.command, config.command);
179 assert_eq!(cloned.timeout_seconds, config.timeout_seconds);
180 }
181
182 #[test]
183 fn test_empty_env() {
184 let config = LspServerConfig::rust_analyzer();
185 assert!(config.env.is_empty());
186 }
187
188 #[test]
189 fn test_multiple_file_patterns() {
190 let config = LspServerConfig::typescript();
191 assert_eq!(config.file_patterns.len(), 2);
192 assert!(config.file_patterns.contains(&"**/*.ts".to_string()));
193 assert!(config.file_patterns.contains(&"**/*.tsx".to_string()));
194 }
195
196 #[test]
197 fn test_initialization_options_none_by_default() {
198 let configs = vec![
199 LspServerConfig::rust_analyzer(),
200 LspServerConfig::pyright(),
201 LspServerConfig::typescript(),
202 ];
203
204 for config in configs {
205 assert!(config.initialization_options.is_none());
206 }
207 }
208}