ricecoder_external_lsp/client/
capabilities.rs

1//! LSP capability negotiation
2
3use serde::{Deserialize, Serialize};
4use serde_json::{json, Value};
5
6/// Client capabilities for LSP initialization
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ClientCapabilities {
9    /// Text document capabilities
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub text_document: Option<TextDocumentClientCapabilities>,
12    /// Workspace capabilities
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub workspace: Option<WorkspaceClientCapabilities>,
15    /// General capabilities
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub general: Option<GeneralClientCapabilities>,
18}
19
20/// Text document client capabilities
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct TextDocumentClientCapabilities {
23    /// Synchronization capabilities
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub synchronization: Option<SynchronizationCapability>,
26    /// Completion capabilities
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub completion: Option<CompletionCapability>,
29    /// Hover capabilities
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub hover: Option<HoverCapability>,
32    /// Diagnostic capabilities
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub publish_diagnostics: Option<PublishDiagnosticsCapability>,
35}
36
37/// Synchronization capability
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct SynchronizationCapability {
40    /// Whether the client supports incremental synchronization
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub did_save: Option<bool>,
43    /// Whether the client supports full document synchronization
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub will_save: Option<bool>,
46}
47
48/// Completion capability
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct CompletionCapability {
51    /// Whether the client supports completion item snippets
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub completion_item: Option<CompletionItemCapability>,
54}
55
56/// Completion item capability
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CompletionItemCapability {
59    /// Whether the client supports snippet syntax
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub snippet_support: Option<bool>,
62}
63
64/// Hover capability
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct HoverCapability {
67    /// Whether the client supports markdown content
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub content_format: Option<Vec<String>>,
70}
71
72/// Publish diagnostics capability
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct PublishDiagnosticsCapability {
75    /// Whether the client supports related information
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub related_information: Option<bool>,
78}
79
80/// Workspace client capabilities
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct WorkspaceClientCapabilities {
83    /// Whether the client supports workspace folders
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub workspace_folders: Option<bool>,
86}
87
88/// General client capabilities
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct GeneralClientCapabilities {
91    /// Whether the client supports regular expressions
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub regular_expressions: Option<RegularExpressionCapability>,
94}
95
96/// Regular expression capability
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct RegularExpressionCapability {
99    /// The regex engine used
100    pub engine: String,
101}
102
103/// Server capabilities from LSP initialization response
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct ServerCapabilities {
106    /// Completion provider capabilities
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub completion_provider: Option<Value>,
109    /// Hover provider capabilities
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub hover_provider: Option<Value>,
112    /// Definition provider capabilities
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub definition_provider: Option<Value>,
115    /// References provider capabilities
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub references_provider: Option<Value>,
118    /// Document symbol provider capabilities
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub document_symbol_provider: Option<Value>,
121    /// Workspace symbol provider capabilities
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub workspace_symbol_provider: Option<Value>,
124    /// Code action provider capabilities
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub code_action_provider: Option<Value>,
127    /// Text document sync capabilities
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub text_document_sync: Option<Value>,
130    /// Diagnostic provider capabilities
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub diagnostic_provider: Option<Value>,
133}
134
135/// Handles LSP capability negotiation
136pub struct CapabilityNegotiator;
137
138impl CapabilityNegotiator {
139    /// Create a new capability negotiator
140    pub fn new() -> Self {
141        Self
142    }
143
144    /// Create default client capabilities for ricecoder
145    pub fn default_client_capabilities() -> ClientCapabilities {
146        ClientCapabilities {
147            text_document: Some(TextDocumentClientCapabilities {
148                synchronization: Some(SynchronizationCapability {
149                    did_save: Some(true),
150                    will_save: Some(false),
151                }),
152                completion: Some(CompletionCapability {
153                    completion_item: Some(CompletionItemCapability {
154                        snippet_support: Some(true),
155                    }),
156                }),
157                hover: Some(HoverCapability {
158                    content_format: Some(vec!["markdown".to_string(), "plaintext".to_string()]),
159                }),
160                publish_diagnostics: Some(PublishDiagnosticsCapability {
161                    related_information: Some(true),
162                }),
163            }),
164            workspace: Some(WorkspaceClientCapabilities {
165                workspace_folders: Some(true),
166            }),
167            general: Some(GeneralClientCapabilities {
168                regular_expressions: Some(RegularExpressionCapability {
169                    engine: "ECMAScript".to_string(),
170                }),
171            }),
172        }
173    }
174
175    /// Create initialization request parameters
176    pub fn create_initialize_params(
177        process_id: Option<u32>,
178        root_path: Option<String>,
179        root_uri: Option<String>,
180    ) -> Value {
181        let mut params = json!({
182            "processId": process_id,
183            "capabilities": Self::default_client_capabilities(),
184        });
185
186        if let Some(root_path) = root_path {
187            params["rootPath"] = json!(root_path);
188        }
189
190        if let Some(root_uri) = root_uri {
191            params["rootUri"] = json!(root_uri);
192        }
193
194        params
195    }
196
197    /// Check if server supports a capability
198    pub fn supports_capability(
199        capabilities: &ServerCapabilities,
200        capability: &str,
201    ) -> bool {
202        match capability {
203            "completion" => capabilities.completion_provider.is_some(),
204            "hover" => capabilities.hover_provider.is_some(),
205            "definition" => capabilities.definition_provider.is_some(),
206            "references" => capabilities.references_provider.is_some(),
207            "documentSymbol" => capabilities.document_symbol_provider.is_some(),
208            "workspaceSymbol" => capabilities.workspace_symbol_provider.is_some(),
209            "codeAction" => capabilities.code_action_provider.is_some(),
210            "diagnostics" => capabilities.diagnostic_provider.is_some(),
211            _ => false,
212        }
213    }
214
215    /// Get list of supported capabilities
216    pub fn get_supported_capabilities(capabilities: &ServerCapabilities) -> Vec<String> {
217        let mut supported = Vec::new();
218
219        if capabilities.completion_provider.is_some() {
220            supported.push("completion".to_string());
221        }
222        if capabilities.hover_provider.is_some() {
223            supported.push("hover".to_string());
224        }
225        if capabilities.definition_provider.is_some() {
226            supported.push("definition".to_string());
227        }
228        if capabilities.references_provider.is_some() {
229            supported.push("references".to_string());
230        }
231        if capabilities.document_symbol_provider.is_some() {
232            supported.push("documentSymbol".to_string());
233        }
234        if capabilities.workspace_symbol_provider.is_some() {
235            supported.push("workspaceSymbol".to_string());
236        }
237        if capabilities.code_action_provider.is_some() {
238            supported.push("codeAction".to_string());
239        }
240        if capabilities.diagnostic_provider.is_some() {
241            supported.push("diagnostics".to_string());
242        }
243
244        supported
245    }
246}
247
248impl Default for CapabilityNegotiator {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_default_client_capabilities() {
260        let caps = CapabilityNegotiator::default_client_capabilities();
261
262        assert!(caps.text_document.is_some());
263        assert!(caps.workspace.is_some());
264        assert!(caps.general.is_some());
265
266        let text_doc = caps.text_document.unwrap();
267        assert!(text_doc.synchronization.is_some());
268        assert!(text_doc.completion.is_some());
269        assert!(text_doc.hover.is_some());
270    }
271
272    #[test]
273    fn test_create_initialize_params() {
274        let params = CapabilityNegotiator::create_initialize_params(
275            Some(1234),
276            Some("/path/to/project".to_string()),
277            Some("file:///path/to/project".to_string()),
278        );
279
280        assert_eq!(params["processId"], 1234);
281        assert_eq!(params["rootPath"], "/path/to/project");
282        assert_eq!(params["rootUri"], "file:///path/to/project");
283        assert!(params["capabilities"].is_object());
284    }
285
286    #[test]
287    fn test_supports_capability() {
288        let caps = ServerCapabilities {
289            completion_provider: Some(json!({})),
290            hover_provider: Some(json!({})),
291            definition_provider: None,
292            references_provider: None,
293            document_symbol_provider: None,
294            workspace_symbol_provider: None,
295            code_action_provider: None,
296            diagnostic_provider: None,
297            text_document_sync: None,
298        };
299
300        assert!(CapabilityNegotiator::supports_capability(&caps, "completion"));
301        assert!(CapabilityNegotiator::supports_capability(&caps, "hover"));
302        assert!(!CapabilityNegotiator::supports_capability(&caps, "definition"));
303    }
304
305    #[test]
306    fn test_get_supported_capabilities() {
307        let caps = ServerCapabilities {
308            completion_provider: Some(json!({})),
309            hover_provider: Some(json!({})),
310            definition_provider: Some(json!({})),
311            references_provider: None,
312            document_symbol_provider: None,
313            workspace_symbol_provider: None,
314            code_action_provider: None,
315            diagnostic_provider: None,
316            text_document_sync: None,
317        };
318
319        let supported = CapabilityNegotiator::get_supported_capabilities(&caps);
320
321        assert!(supported.contains(&"completion".to_string()));
322        assert!(supported.contains(&"hover".to_string()));
323        assert!(supported.contains(&"definition".to_string()));
324        assert_eq!(supported.len(), 3);
325    }
326}