ricecoder_lsp/
proxy.rs

1//! LSP Proxy for external LSP server integration
2//!
3//! This module provides a proxy layer that routes requests to external LSP servers
4//! while maintaining backward compatibility with internal providers.
5//!
6//! # Architecture
7//!
8//! The proxy acts as a middleware between ricecoder's LSP server and external LSP servers:
9//!
10//! ```text
11//! ricecoder-lsp (LspServer)
12//!     ↓
13//! LspProxy (routes requests)
14//!     ↓
15//! External LSP Servers (rust-analyzer, tsserver, pylsp, etc.)
16//! ```
17//!
18//! # Request Routing
19//!
20//! Requests are routed based on language:
21//! - If external LSP is configured for the language → forward to external LSP
22//! - If external LSP is unavailable → fall back to internal provider
23//! - If no external LSP configured → use internal provider
24
25use crate::types::{LspError, LspResult};
26use serde_json::Value;
27use std::sync::Arc;
28use tracing::{debug, info, warn};
29
30/// LSP Proxy for routing requests to external LSP servers
31///
32/// This proxy maintains backward compatibility while enabling external LSP integration.
33pub struct LspProxy {
34    /// External LSP client pool (optional)
35    external_lsp: Option<Arc<dyn ExternalLspClient>>,
36    /// Enable fallback to internal providers
37    enable_fallback: bool,
38}
39
40/// Trait for external LSP client
41pub trait ExternalLspClient: Send + Sync {
42    /// Forward completion request to external LSP
43    fn forward_completion(
44        &self,
45        language: &str,
46        uri: &str,
47        position: Value,
48        context: Value,
49    ) -> LspResult<Option<Value>>;
50
51    /// Forward diagnostics request to external LSP
52    fn forward_diagnostics(
53        &self,
54        language: &str,
55        uri: &str,
56    ) -> LspResult<Option<Value>>;
57
58    /// Forward hover request to external LSP
59    fn forward_hover(
60        &self,
61        language: &str,
62        uri: &str,
63        position: Value,
64    ) -> LspResult<Option<Value>>;
65
66    /// Forward definition request to external LSP
67    fn forward_definition(
68        &self,
69        language: &str,
70        uri: &str,
71        position: Value,
72    ) -> LspResult<Option<Value>>;
73
74    /// Forward references request to external LSP
75    fn forward_references(
76        &self,
77        language: &str,
78        uri: &str,
79        position: Value,
80    ) -> LspResult<Option<Value>>;
81
82    /// Check if external LSP is available for language
83    fn is_available(&self, language: &str) -> bool;
84}
85
86impl LspProxy {
87    /// Create a new LSP proxy without external LSP
88    pub fn new() -> Self {
89        Self {
90            external_lsp: None,
91            enable_fallback: true,
92        }
93    }
94
95    /// Create a new LSP proxy with external LSP client
96    pub fn with_external_lsp(
97        external_lsp: Arc<dyn ExternalLspClient>,
98        enable_fallback: bool,
99    ) -> Self {
100        Self {
101            external_lsp: Some(external_lsp),
102            enable_fallback,
103        }
104    }
105
106    /// Route completion request
107    ///
108    /// # Arguments
109    ///
110    /// * `language` - Programming language
111    /// * `uri` - Document URI
112    /// * `position` - Cursor position
113    /// * `context` - Completion context
114    /// * `fallback_fn` - Fallback function for internal provider
115    ///
116    /// # Returns
117    ///
118    /// Completion items from external LSP or fallback provider
119    pub fn route_completion<F>(
120        &self,
121        language: &str,
122        uri: &str,
123        position: Value,
124        context: Value,
125        fallback_fn: F,
126    ) -> LspResult<Value>
127    where
128        F: FnOnce() -> LspResult<Value>,
129    {
130        // Try external LSP first
131        if let Some(external_lsp) = &self.external_lsp {
132            if external_lsp.is_available(language) {
133                debug!("Routing completion to external LSP for language: {}", language);
134                match external_lsp.forward_completion(language, uri, position, context) {
135                    Ok(Some(result)) => {
136                        info!("Received completion from external LSP");
137                        return Ok(result);
138                    }
139                    Ok(None) => {
140                        debug!("External LSP returned no completions");
141                    }
142                    Err(e) => {
143                        warn!("External LSP completion failed: {}", e);
144                        if !self.enable_fallback {
145                            return Err(e);
146                        }
147                    }
148                }
149            }
150        }
151
152        // Fall back to internal provider
153        if self.enable_fallback {
154            debug!("Falling back to internal completion provider");
155            fallback_fn()
156        } else {
157            Err(LspError::InternalError(
158                "External LSP unavailable and fallback disabled".to_string(),
159            ))
160        }
161    }
162
163    /// Route diagnostics request
164    ///
165    /// # Arguments
166    ///
167    /// * `language` - Programming language
168    /// * `uri` - Document URI
169    /// * `fallback_fn` - Fallback function for internal provider
170    ///
171    /// # Returns
172    ///
173    /// Diagnostics from external LSP or fallback provider
174    pub fn route_diagnostics<F>(
175        &self,
176        language: &str,
177        uri: &str,
178        fallback_fn: F,
179    ) -> LspResult<Value>
180    where
181        F: FnOnce() -> LspResult<Value>,
182    {
183        // Try external LSP first
184        if let Some(external_lsp) = &self.external_lsp {
185            if external_lsp.is_available(language) {
186                debug!("Routing diagnostics to external LSP for language: {}", language);
187                match external_lsp.forward_diagnostics(language, uri) {
188                    Ok(Some(result)) => {
189                        info!("Received diagnostics from external LSP");
190                        return Ok(result);
191                    }
192                    Ok(None) => {
193                        debug!("External LSP returned no diagnostics");
194                    }
195                    Err(e) => {
196                        warn!("External LSP diagnostics failed: {}", e);
197                        if !self.enable_fallback {
198                            return Err(e);
199                        }
200                    }
201                }
202            }
203        }
204
205        // Fall back to internal provider
206        if self.enable_fallback {
207            debug!("Falling back to internal diagnostics provider");
208            fallback_fn()
209        } else {
210            Err(LspError::InternalError(
211                "External LSP unavailable and fallback disabled".to_string(),
212            ))
213        }
214    }
215
216    /// Route hover request
217    ///
218    /// # Arguments
219    ///
220    /// * `language` - Programming language
221    /// * `uri` - Document URI
222    /// * `position` - Cursor position
223    /// * `fallback_fn` - Fallback function for internal provider
224    ///
225    /// # Returns
226    ///
227    /// Hover information from external LSP or fallback provider
228    pub fn route_hover<F>(
229        &self,
230        language: &str,
231        uri: &str,
232        position: Value,
233        fallback_fn: F,
234    ) -> LspResult<Value>
235    where
236        F: FnOnce() -> LspResult<Value>,
237    {
238        // Try external LSP first
239        if let Some(external_lsp) = &self.external_lsp {
240            if external_lsp.is_available(language) {
241                debug!("Routing hover to external LSP for language: {}", language);
242                match external_lsp.forward_hover(language, uri, position) {
243                    Ok(Some(result)) => {
244                        info!("Received hover from external LSP");
245                        return Ok(result);
246                    }
247                    Ok(None) => {
248                        debug!("External LSP returned no hover information");
249                    }
250                    Err(e) => {
251                        warn!("External LSP hover failed: {}", e);
252                        if !self.enable_fallback {
253                            return Err(e);
254                        }
255                    }
256                }
257            }
258        }
259
260        // Fall back to internal provider
261        if self.enable_fallback {
262            debug!("Falling back to internal hover provider");
263            fallback_fn()
264        } else {
265            Err(LspError::InternalError(
266                "External LSP unavailable and fallback disabled".to_string(),
267            ))
268        }
269    }
270
271    /// Route definition request
272    ///
273    /// # Arguments
274    ///
275    /// * `language` - Programming language
276    /// * `uri` - Document URI
277    /// * `position` - Cursor position
278    /// * `fallback_fn` - Fallback function for internal provider
279    ///
280    /// # Returns
281    ///
282    /// Definition locations from external LSP or fallback provider
283    pub fn route_definition<F>(
284        &self,
285        language: &str,
286        uri: &str,
287        position: Value,
288        fallback_fn: F,
289    ) -> LspResult<Value>
290    where
291        F: FnOnce() -> LspResult<Value>,
292    {
293        // Try external LSP first
294        if let Some(external_lsp) = &self.external_lsp {
295            if external_lsp.is_available(language) {
296                debug!("Routing definition to external LSP for language: {}", language);
297                match external_lsp.forward_definition(language, uri, position) {
298                    Ok(Some(result)) => {
299                        info!("Received definition from external LSP");
300                        return Ok(result);
301                    }
302                    Ok(None) => {
303                        debug!("External LSP returned no definition");
304                    }
305                    Err(e) => {
306                        warn!("External LSP definition failed: {}", e);
307                        if !self.enable_fallback {
308                            return Err(e);
309                        }
310                    }
311                }
312            }
313        }
314
315        // Fall back to internal provider
316        if self.enable_fallback {
317            debug!("Falling back to internal definition provider");
318            fallback_fn()
319        } else {
320            Err(LspError::InternalError(
321                "External LSP unavailable and fallback disabled".to_string(),
322            ))
323        }
324    }
325
326    /// Route references request
327    ///
328    /// # Arguments
329    ///
330    /// * `language` - Programming language
331    /// * `uri` - Document URI
332    /// * `position` - Cursor position
333    /// * `fallback_fn` - Fallback function for internal provider
334    ///
335    /// # Returns
336    ///
337    /// Reference locations from external LSP or fallback provider
338    pub fn route_references<F>(
339        &self,
340        language: &str,
341        uri: &str,
342        position: Value,
343        fallback_fn: F,
344    ) -> LspResult<Value>
345    where
346        F: FnOnce() -> LspResult<Value>,
347    {
348        // Try external LSP first
349        if let Some(external_lsp) = &self.external_lsp {
350            if external_lsp.is_available(language) {
351                debug!("Routing references to external LSP for language: {}", language);
352                match external_lsp.forward_references(language, uri, position) {
353                    Ok(Some(result)) => {
354                        info!("Received references from external LSP");
355                        return Ok(result);
356                    }
357                    Ok(None) => {
358                        debug!("External LSP returned no references");
359                    }
360                    Err(e) => {
361                        warn!("External LSP references failed: {}", e);
362                        if !self.enable_fallback {
363                            return Err(e);
364                        }
365                    }
366                }
367            }
368        }
369
370        // Fall back to internal provider
371        if self.enable_fallback {
372            debug!("Falling back to internal references provider");
373            fallback_fn()
374        } else {
375            Err(LspError::InternalError(
376                "External LSP unavailable and fallback disabled".to_string(),
377            ))
378        }
379    }
380
381    /// Check if external LSP is available for language
382    pub fn is_external_lsp_available(&self, language: &str) -> bool {
383        self.external_lsp
384            .as_ref()
385            .map(|lsp| lsp.is_available(language))
386            .unwrap_or(false)
387    }
388}
389
390impl Default for LspProxy {
391    fn default() -> Self {
392        Self::new()
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    #[test]
401    fn test_proxy_creation() {
402        let proxy = LspProxy::new();
403        assert!(!proxy.is_external_lsp_available("rust"));
404    }
405
406    #[test]
407    fn test_proxy_fallback() {
408        let proxy = LspProxy::new();
409        let result = proxy.route_completion(
410            "rust",
411            "file:///test.rs",
412            Value::Null,
413            Value::Null,
414            || Ok(Value::Array(vec![])),
415        );
416        assert!(result.is_ok());
417    }
418}