ricecoder_external_lsp/
semantic.rs

1//! Semantic feature integration (completion, diagnostics, hover, navigation)
2//!
3//! This module provides forwarding and merging of semantic features from external LSP servers.
4
5use crate::client::LspConnection;
6use crate::error::Result;
7use crate::mapping::{CompletionMapper, DiagnosticsMapper, HoverMapper};
8use crate::types::{CompletionMappingRules, HoverMappingRules, MergeConfig};
9use ricecoder_completion::types::{CompletionContext, CompletionItem};
10use ricecoder_lsp::types::{Diagnostic, Position, Range};
11use serde_json::{json, Value};
12use std::time::Duration;
13
14/// Semantic feature forwarder and merger
15pub struct SemanticFeatures {
16    /// LSP connection for forwarding requests
17    connection: std::sync::Arc<LspConnection>,
18    /// Completion mapper for transforming LSP responses
19    completion_mapper: CompletionMapper,
20    /// Diagnostics mapper for transforming LSP responses
21    #[allow(dead_code)]
22    diagnostics_mapper: DiagnosticsMapper,
23    /// Hover mapper for transforming LSP responses
24    hover_mapper: HoverMapper,
25    /// Merge configuration
26    #[allow(dead_code)]
27    merge_config: MergeConfig,
28    /// Request timeout
29    timeout: Duration,
30}
31
32impl SemanticFeatures {
33    /// Create a new semantic features handler
34    pub fn new(
35        connection: std::sync::Arc<LspConnection>,
36        completion_mapper: CompletionMapper,
37        diagnostics_mapper: DiagnosticsMapper,
38        hover_mapper: HoverMapper,
39        merge_config: MergeConfig,
40        timeout: Duration,
41    ) -> Self {
42        Self {
43            connection,
44            completion_mapper,
45            diagnostics_mapper,
46            hover_mapper,
47            merge_config,
48            timeout,
49        }
50    }
51
52    /// Forward completion request to external LSP server
53    ///
54    /// # Arguments
55    ///
56    /// * `uri` - Document URI
57    /// * `position` - Cursor position
58    /// * `context` - Completion context
59    ///
60    /// # Returns
61    ///
62    /// Vector of completion items from external LSP, or None if unavailable
63    pub async fn forward_completion(
64        &self,
65        uri: &str,
66        position: Position,
67        _context: &CompletionContext,
68    ) -> Result<Option<Vec<CompletionItem>>> {
69        // Create textDocument/completion request
70        let params = json!({
71            "textDocument": {
72                "uri": uri
73            },
74            "position": {
75                "line": position.line,
76                "character": position.character
77            }
78        });
79
80        // Send request to LSP server
81        let (_request, mut rx) = self
82            .connection
83            .create_tracked_request("textDocument/completion", Some(params), self.timeout)
84            .await?;
85
86        // Wait for response with timeout
87        match tokio::time::timeout(self.timeout, &mut rx).await {
88            Ok(Ok(result)) => {
89                // Parse and transform response
90                match result {
91                    Ok(response) => {
92                        // Transform LSP completion response to ricecoder CompletionItem
93                        // Use default mapping rules for standard LSP response format
94                        let rules = CompletionMappingRules {
95                            items_path: "$.result.items".to_string(),
96                            field_mappings: Default::default(),
97                            transform: None,
98                        };
99                        
100                        let mapped_items = self.completion_mapper.map(&response, &rules)?;
101                        
102                        // Convert mapped items to CompletionItem
103                        let items = mapped_items
104                            .into_iter()
105                            .filter_map(|item| {
106                                let label = item.get("label")?.as_str()?.to_string();
107                                let insert_text = item.get("insertText")
108                                    .and_then(|v| v.as_str())
109                                    .unwrap_or(&label)
110                                    .to_string();
111                                
112                                Some(CompletionItem::new(
113                                    label,
114                                    ricecoder_completion::types::CompletionItemKind::Variable,
115                                    insert_text,
116                                ))
117                            })
118                            .collect();
119                        
120                        Ok(Some(items))
121                    }
122                    Err(e) => {
123                        // Log error but don't fail - will fall back to internal provider
124                        tracing::warn!("LSP completion request failed: {}", e);
125                        Ok(None)
126                    }
127                }
128            }
129            Ok(Err(_)) => {
130                // Receiver was dropped
131                Ok(None)
132            }
133            Err(_) => {
134                // Timeout
135                tracing::warn!("LSP completion request timed out");
136                Ok(None)
137            }
138        }
139    }
140
141    /// Forward diagnostics request to external LSP server
142    ///
143    /// # Arguments
144    ///
145    /// * `_uri` - Document URI
146    ///
147    /// # Returns
148    ///
149    /// Vector of diagnostics from external LSP, or None if unavailable
150    pub async fn forward_diagnostics(&self, _uri: &str) -> Result<Option<Vec<Diagnostic>>> {
151        // Note: Diagnostics are typically pushed by the server via textDocument/publishDiagnostics
152        // This method is for requesting diagnostics on demand if needed
153        // For now, we return None as diagnostics are handled via notifications
154
155        // In a full implementation, you might send a custom request or use a language-specific
156        // extension to request diagnostics on demand
157        Ok(None)
158    }
159
160    /// Forward hover request to external LSP server
161    ///
162    /// # Arguments
163    ///
164    /// * `uri` - Document URI
165    /// * `position` - Cursor position
166    ///
167    /// # Returns
168    ///
169    /// Hover information from external LSP, or None if unavailable
170    pub async fn forward_hover(&self, uri: &str, position: Position) -> Result<Option<String>> {
171        // Create textDocument/hover request
172        let params = json!({
173            "textDocument": {
174                "uri": uri
175            },
176            "position": {
177                "line": position.line,
178                "character": position.character
179            }
180        });
181
182        // Send request to LSP server
183        let (_request, mut rx) = self
184            .connection
185            .create_tracked_request("textDocument/hover", Some(params), self.timeout)
186            .await?;
187
188        // Wait for response with timeout
189        match tokio::time::timeout(self.timeout, &mut rx).await {
190            Ok(Ok(result)) => {
191                // Parse and transform response
192                match result {
193                    Ok(response) => {
194                        // Transform LSP hover response to ricecoder format
195                        let rules = HoverMappingRules {
196                            content_path: "$.result.contents".to_string(),
197                            field_mappings: Default::default(),
198                            transform: None,
199                        };
200                        
201                        let hover_value = self.hover_mapper.map(&response, &rules)?;
202                        
203                        // Convert Value to String
204                        let hover_info = if let Some(s) = hover_value.as_str() {
205                            s.to_string()
206                        } else {
207                            hover_value.to_string()
208                        };
209                        
210                        Ok(Some(hover_info))
211                    }
212                    Err(e) => {
213                        // Log error but don't fail - will fall back to internal provider
214                        tracing::warn!("LSP hover request failed: {}", e);
215                        Ok(None)
216                    }
217                }
218            }
219            Ok(Err(_)) => {
220                // Receiver was dropped
221                Ok(None)
222            }
223            Err(_) => {
224                // Timeout
225                tracing::warn!("LSP hover request timed out");
226                Ok(None)
227            }
228        }
229    }
230
231    /// Forward definition request to external LSP server
232    ///
233    /// # Arguments
234    ///
235    /// * `uri` - Document URI
236    /// * `position` - Cursor position
237    ///
238    /// # Returns
239    ///
240    /// Vector of definition locations from external LSP, or None if unavailable
241    pub async fn forward_definition(
242        &self,
243        uri: &str,
244        position: Position,
245    ) -> Result<Option<Vec<(String, Range)>>> {
246        // Create textDocument/definition request
247        let params = json!({
248            "textDocument": {
249                "uri": uri
250            },
251            "position": {
252                "line": position.line,
253                "character": position.character
254            }
255        });
256
257        // Send request to LSP server
258        let (_request, mut rx) = self
259            .connection
260            .create_tracked_request("textDocument/definition", Some(params), self.timeout)
261            .await?;
262
263        // Wait for response with timeout
264        match tokio::time::timeout(self.timeout, &mut rx).await {
265            Ok(Ok(result)) => {
266                // Parse response
267                match result {
268                    Ok(response) => {
269                        // Parse definition locations from response
270                        let locations = parse_locations(&response)?;
271                        Ok(Some(locations))
272                    }
273                    Err(e) => {
274                        // Log error but don't fail - will fall back to internal provider
275                        tracing::warn!("LSP definition request failed: {}", e);
276                        Ok(None)
277                    }
278                }
279            }
280            Ok(Err(_)) => {
281                // Receiver was dropped
282                Ok(None)
283            }
284            Err(_) => {
285                // Timeout
286                tracing::warn!("LSP definition request timed out");
287                Ok(None)
288            }
289        }
290    }
291
292    /// Forward references request to external LSP server
293    ///
294    /// # Arguments
295    ///
296    /// * `uri` - Document URI
297    /// * `position` - Cursor position
298    ///
299    /// # Returns
300    ///
301    /// Vector of reference locations from external LSP, or None if unavailable
302    pub async fn forward_references(
303        &self,
304        uri: &str,
305        position: Position,
306    ) -> Result<Option<Vec<(String, Range)>>> {
307        // Create textDocument/references request
308        let params = json!({
309            "textDocument": {
310                "uri": uri
311            },
312            "position": {
313                "line": position.line,
314                "character": position.character
315            },
316            "context": {
317                "includeDeclaration": true
318            }
319        });
320
321        // Send request to LSP server
322        let (_request, mut rx) = self
323            .connection
324            .create_tracked_request("textDocument/references", Some(params), self.timeout)
325            .await?;
326
327        // Wait for response with timeout
328        match tokio::time::timeout(self.timeout, &mut rx).await {
329            Ok(Ok(result)) => {
330                // Parse response
331                match result {
332                    Ok(response) => {
333                        // Parse reference locations from response
334                        let locations = parse_locations(&response)?;
335                        Ok(Some(locations))
336                    }
337                    Err(e) => {
338                        // Log error but don't fail - will fall back to internal provider
339                        tracing::warn!("LSP references request failed: {}", e);
340                        Ok(None)
341                    }
342                }
343            }
344            Ok(Err(_)) => {
345                // Receiver was dropped
346                Ok(None)
347            }
348            Err(_) => {
349                // Timeout
350                tracing::warn!("LSP references request timed out");
351                Ok(None)
352            }
353        }
354    }
355}
356
357/// Parse location information from LSP response
358fn parse_locations(response: &Value) -> Result<Vec<(String, Range)>> {
359    let mut locations = Vec::new();
360
361    // Handle both single location and array of locations
362    let items = if response.is_array() {
363        response.as_array().unwrap().clone()
364    } else if response.is_object() {
365        vec![response.clone()]
366    } else {
367        return Ok(locations);
368    };
369
370    for item in items {
371        if let (Some(uri), Some(range)) = (item.get("uri").and_then(|v| v.as_str()), item.get("range")) {
372            if let Some(parsed_range) = parse_range(range) {
373                locations.push((uri.to_string(), parsed_range));
374            }
375        }
376    }
377
378    Ok(locations)
379}
380
381/// Parse range information from LSP response
382fn parse_range(range: &Value) -> Option<Range> {
383    let start = range.get("start")?;
384    let end = range.get("end")?;
385
386    let start_line = start.get("line")?.as_u64()? as u32;
387    let start_char = start.get("character")?.as_u64()? as u32;
388    let end_line = end.get("line")?.as_u64()? as u32;
389    let end_char = end.get("character")?.as_u64()? as u32;
390
391    Some(Range::new(
392        Position::new(start_line, start_char),
393        Position::new(end_line, end_char),
394    ))
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_parse_single_location() {
403        let response = json!({
404            "uri": "file:///test.rs",
405            "range": {
406                "start": {"line": 0, "character": 0},
407                "end": {"line": 0, "character": 5}
408            }
409        });
410
411        let locations = parse_locations(&response).unwrap();
412        assert_eq!(locations.len(), 1);
413        assert_eq!(locations[0].0, "file:///test.rs");
414        assert_eq!(locations[0].1.start.line, 0);
415        assert_eq!(locations[0].1.start.character, 0);
416        assert_eq!(locations[0].1.end.line, 0);
417        assert_eq!(locations[0].1.end.character, 5);
418    }
419
420    #[test]
421    fn test_parse_multiple_locations() {
422        let response = json!([
423            {
424                "uri": "file:///test1.rs",
425                "range": {
426                    "start": {"line": 0, "character": 0},
427                    "end": {"line": 0, "character": 5}
428                }
429            },
430            {
431                "uri": "file:///test2.rs",
432                "range": {
433                    "start": {"line": 1, "character": 10},
434                    "end": {"line": 1, "character": 15}
435                }
436            }
437        ]);
438
439        let locations = parse_locations(&response).unwrap();
440        assert_eq!(locations.len(), 2);
441        assert_eq!(locations[0].0, "file:///test1.rs");
442        assert_eq!(locations[1].0, "file:///test2.rs");
443    }
444
445    #[test]
446    fn test_parse_empty_response() {
447        let response = json!([]);
448        let locations = parse_locations(&response).unwrap();
449        assert_eq!(locations.len(), 0);
450    }
451
452    #[test]
453    fn test_parse_invalid_range() {
454        let response = json!({
455            "uri": "file:///test.rs",
456            "range": {
457                "start": {"line": "invalid"},
458                "end": {"line": 0, "character": 5}
459            }
460        });
461
462        let locations = parse_locations(&response).unwrap();
463        assert_eq!(locations.len(), 0);
464    }
465}