mcpls_core/config/
server.rs

1//! LSP server configuration types.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7/// Configuration for a single LSP server.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct LspServerConfig {
11    /// Language identifier (e.g., "rust", "python", "typescript").
12    pub language_id: String,
13
14    /// Command to start the LSP server.
15    pub command: String,
16
17    /// Arguments to pass to the LSP server command.
18    #[serde(default)]
19    pub args: Vec<String>,
20
21    /// Environment variables for the LSP server process.
22    #[serde(default)]
23    pub env: HashMap<String, String>,
24
25    /// File patterns this server handles (glob patterns).
26    #[serde(default)]
27    pub file_patterns: Vec<String>,
28
29    /// LSP initialization options (server-specific).
30    #[serde(default)]
31    pub initialization_options: Option<serde_json::Value>,
32
33    /// Request timeout in seconds.
34    #[serde(default = "default_timeout")]
35    pub timeout_seconds: u64,
36}
37
38const fn default_timeout() -> u64 {
39    30
40}
41
42impl LspServerConfig {
43    /// Create a default configuration for rust-analyzer.
44    #[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    /// Create a default configuration for pyright.
58    #[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    /// Create a default configuration for TypeScript language server.
72    #[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}