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}