ricecoder_completion/
external_lsp_proxy.rs

1//! External LSP proxy for completion engine
2//!
3//! This module provides a proxy layer that routes completion 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 the completion engine and external LSP servers:
9//!
10//! ```text
11//! CompletionEngine
12//!     ↓
13//! ExternalLspCompletionProxy (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::{CompletionItem, CompletionResult, Position};
26use async_trait::async_trait;
27use std::sync::Arc;
28use tracing::{debug, info, warn};
29
30/// Trait for external LSP completion client
31#[async_trait]
32pub trait ExternalLspCompletionClient: Send + Sync {
33    /// Forward completion request to external LSP
34    ///
35    /// # Arguments
36    ///
37    /// * `language` - Programming language
38    /// * `uri` - Document URI
39    /// * `code` - Source code
40    /// * `position` - Cursor position
41    ///
42    /// # Returns
43    ///
44    /// Completion items from external LSP, or None if unavailable
45    async fn forward_completion(
46        &self,
47        language: &str,
48        uri: &str,
49        code: &str,
50        position: Position,
51    ) -> CompletionResult<Option<Vec<CompletionItem>>>;
52
53    /// Check if external LSP is available for language
54    fn is_available(&self, language: &str) -> bool;
55}
56
57/// External LSP completion proxy
58///
59/// This proxy maintains backward compatibility while enabling external LSP integration.
60pub struct ExternalLspCompletionProxy {
61    /// External LSP client (optional)
62    external_lsp: Option<Arc<dyn ExternalLspCompletionClient>>,
63    /// Enable fallback to internal providers
64    enable_fallback: bool,
65}
66
67impl ExternalLspCompletionProxy {
68    /// Create a new completion proxy without external LSP
69    pub fn new() -> Self {
70        Self {
71            external_lsp: None,
72            enable_fallback: true,
73        }
74    }
75
76    /// Create a new completion proxy with external LSP client
77    pub fn with_external_lsp(
78        external_lsp: Arc<dyn ExternalLspCompletionClient>,
79        enable_fallback: bool,
80    ) -> Self {
81        Self {
82            external_lsp: Some(external_lsp),
83            enable_fallback,
84        }
85    }
86
87    /// Route completion request
88    ///
89    /// # Arguments
90    ///
91    /// * `language` - Programming language
92    /// * `uri` - Document URI
93    /// * `code` - Source code
94    /// * `position` - Cursor position
95    /// * `fallback_fn` - Fallback function for internal provider
96    ///
97    /// # Returns
98    ///
99    /// Completion items from external LSP or fallback provider
100    pub async fn route_completion<F>(
101        &self,
102        language: &str,
103        uri: &str,
104        code: &str,
105        position: Position,
106        fallback_fn: F,
107    ) -> CompletionResult<Vec<CompletionItem>>
108    where
109        F: std::future::Future<Output = CompletionResult<Vec<CompletionItem>>>,
110    {
111        // Try external LSP first
112        if let Some(external_lsp) = &self.external_lsp {
113            if external_lsp.is_available(language) {
114                debug!("Routing completion to external LSP for language: {}", language);
115                match external_lsp
116                    .forward_completion(language, uri, code, position)
117                    .await
118                {
119                    Ok(Some(items)) => {
120                        info!("Received {} completions from external LSP", items.len());
121                        return Ok(items);
122                    }
123                    Ok(None) => {
124                        debug!("External LSP returned no completions");
125                    }
126                    Err(e) => {
127                        warn!("External LSP completion failed: {}", e);
128                        if !self.enable_fallback {
129                            return Err(e);
130                        }
131                    }
132                }
133            }
134        }
135
136        // Fall back to internal provider
137        if self.enable_fallback {
138            debug!("Falling back to internal completion provider");
139            fallback_fn.await
140        } else {
141            Err(crate::types::CompletionError::InternalError(
142                "External LSP unavailable and fallback disabled".to_string(),
143            ))
144        }
145    }
146
147    /// Check if external LSP is available for language
148    pub fn is_external_lsp_available(&self, language: &str) -> bool {
149        self.external_lsp
150            .as_ref()
151            .map(|lsp| lsp.is_available(language))
152            .unwrap_or(false)
153    }
154}
155
156impl Default for ExternalLspCompletionProxy {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_proxy_creation() {
168        let proxy = ExternalLspCompletionProxy::new();
169        assert!(!proxy.is_external_lsp_available("rust"));
170    }
171
172    #[tokio::test]
173    async fn test_proxy_fallback() {
174        let proxy = ExternalLspCompletionProxy::new();
175        let result = proxy
176            .route_completion(
177                "rust",
178                "file:///test.rs",
179                "fn main() {}",
180                Position::new(0, 0),
181                async { Ok(vec![]) },
182            )
183            .await;
184        assert!(result.is_ok());
185    }
186}