Skip to main content

lens_core/lsp/
mod.rs

1//! LSP Integration Module
2//! 
3//! Provides real Language Server Protocol integration for:
4//! - tsserver (TypeScript/JavaScript) 
5//! - pylsp (Python)
6//! - rust-analyzer (Rust)
7//! - gopls (Go)
8//!
9//! Key features:
10//! - Bounded BFS traversal (depth≤2, K≤64) for def/ref/type/impl
11//! - 24h TTL hint caching with invalidation
12//! - 40-60% routing by intent with safety floors
13//! - Real language servers with process management
14
15pub mod client;
16pub mod hint;
17pub mod manager;
18pub mod router;
19pub mod server_process;
20
21use anyhow::Result;
22use lsp_types::*;
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25use std::path::PathBuf;
26use tokio::sync::RwLock;
27
28pub use client::LspClient;
29pub use hint::{HintCache, SymbolHint, HintType};
30pub use manager::LspManager;
31pub use router::LspRouter;
32pub use server_process::LspServerProcess;
33
34/// Supported LSP server types
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub enum LspServerType {
37    TypeScript,  // tsserver
38    Python,      // pylsp
39    Rust,        // rust-analyzer
40    Go,          // gopls
41    JavaScript,  // tsserver
42}
43
44impl LspServerType {
45    pub fn from_file_extension(ext: &str) -> Option<Self> {
46        match ext.to_lowercase().as_str() {
47            "ts" | "tsx" => Some(Self::TypeScript),
48            "js" | "jsx" => Some(Self::JavaScript),
49            "py" | "pyi" => Some(Self::Python),
50            "rs" => Some(Self::Rust),
51            "go" => Some(Self::Go),
52            _ => None,
53        }
54    }
55
56    pub fn server_command(&self) -> (&'static str, Vec<&'static str>) {
57        match self {
58            Self::TypeScript | Self::JavaScript => ("typescript-language-server", vec!["--stdio"]),
59            Self::Python => ("pylsp", vec!["--verbose"]),
60            Self::Rust => ("rust-analyzer", vec![]),
61            Self::Go => ("gopls", vec!["serve"]),
62        }
63    }
64
65    pub fn file_extensions(&self) -> Vec<&'static str> {
66        match self {
67            Self::TypeScript => vec!["ts", "tsx"],
68            Self::JavaScript => vec!["js", "jsx"],
69            Self::Python => vec!["py", "pyi"],
70            Self::Rust => vec!["rs"],
71            Self::Go => vec!["go"],
72        }
73    }
74}
75
76/// LSP query intent classification
77#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub enum QueryIntent {
79    /// Find definition of symbol
80    Definition,
81    /// Find references to symbol  
82    References,
83    /// Find type information
84    TypeDefinition,
85    /// Find implementations
86    Implementation,
87    /// Goto declaration
88    Declaration,
89    /// Symbol search
90    Symbol,
91    /// Completion/autocomplete
92    Completion,
93    /// Hover information
94    Hover,
95    /// General text search (fallback to basic search)
96    TextSearch,
97}
98
99impl QueryIntent {
100    /// Classify query intent from search string
101    pub fn classify(query: &str) -> Self {
102        let query_lower = query.to_lowercase();
103        
104        // Pattern matching for intent classification
105        if query_lower.contains("def ") || query_lower.contains("function ") || query_lower.contains("class ") {
106            Self::Definition
107        } else if query_lower.contains("ref ") || query_lower.contains("usage") || query_lower.contains("usages") {
108            Self::References
109        } else if query_lower.contains("type ") || query_lower.contains("interface ") {
110            Self::TypeDefinition
111        } else if query_lower.contains("impl ") || query_lower.contains("implement") {
112            Self::Implementation
113        } else if query_lower.starts_with("@") {
114            Self::Symbol
115        } else if query_lower.ends_with("?") {
116            Self::Hover
117        } else {
118            Self::TextSearch
119        }
120    }
121
122    /// Check if this intent is LSP-eligible
123    pub fn is_lsp_eligible(&self) -> bool {
124        !matches!(self, Self::TextSearch)
125    }
126    
127    /// Check if this intent requires safety floor (monotone results)
128    /// 
129    /// Exact and structural queries must never return fewer results than baseline
130    /// This implements the TODO.md safety requirement for exact/struct queries
131    pub fn requires_safety_floor(&self) -> bool {
132        matches!(self, Self::Definition | Self::Symbol | Self::TypeDefinition | Self::Implementation)
133    }
134    
135    /// Check if this is an exact match query that must be monotone
136    pub fn is_exact_query(&self) -> bool {
137        matches!(self, Self::Definition | Self::Symbol)
138    }
139    
140    /// Check if this is a structural query that must be monotone
141    pub fn is_structural_query(&self) -> bool {
142        matches!(self, Self::TypeDefinition | Self::Implementation)
143    }
144}
145
146/// BFS traversal bounds for LSP queries
147#[derive(Debug, Clone)]
148pub struct TraversalBounds {
149    pub max_depth: u8,
150    pub max_results: u16,
151    pub timeout_ms: u64,
152}
153
154impl Default for TraversalBounds {
155    fn default() -> Self {
156        Self {
157            max_depth: 2,    // depth ≤ 2 per TODO.md
158            max_results: 64, // K ≤ 64 per TODO.md
159            timeout_ms: 5000, // 5 second default timeout
160        }
161    }
162}
163
164/// LSP search result with provenance
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct LspSearchResult {
167    pub file_path: String,
168    pub line_number: u32,
169    pub column: u32,
170    pub content: String,
171    pub hint_type: HintType,
172    pub server_type: LspServerType,
173    pub confidence: f64,
174    pub context_lines: Option<Vec<String>>,
175}
176
177/// LSP-augmented search response
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct LspSearchResponse {
180    pub lsp_results: Vec<LspSearchResult>,
181    pub fallback_results: Vec<crate::search::SearchResult>,
182    pub total_time_ms: u64,
183    pub lsp_time_ms: u64,
184    pub cache_hit_rate: f64,
185    pub server_types_used: Vec<LspServerType>,
186    pub intent: QueryIntent,
187}
188
189impl Default for LspSearchResponse {
190    fn default() -> Self {
191        Self {
192            lsp_results: Vec::new(),
193            fallback_results: Vec::new(),
194            total_time_ms: 0,
195            lsp_time_ms: 0,
196            cache_hit_rate: 0.0,
197            server_types_used: Vec::new(),
198            intent: QueryIntent::TextSearch,
199        }
200    }
201}
202
203/// Configuration for LSP integration
204#[derive(Debug, Clone)]
205pub struct LspConfig {
206    pub enabled: bool,
207    pub server_timeout_ms: u64,
208    pub cache_ttl_hours: u64,
209    pub max_concurrent_requests: usize,
210    pub routing_percentage: f64, // Target 40-60% per TODO.md
211    pub traversal_bounds: TraversalBounds,
212}
213
214impl Default for LspConfig {
215    fn default() -> Self {
216        Self {
217            enabled: true,
218            server_timeout_ms: 5000,
219            cache_ttl_hours: 24,
220            max_concurrent_requests: 10,
221            routing_percentage: 0.5, // 50% routing target
222            traversal_bounds: TraversalBounds::default(),
223        }
224    }
225}
226
227/// Global LSP state manager
228pub struct LspState {
229    manager: RwLock<Option<LspManager>>,
230    config: LspConfig,
231}
232
233impl LspState {
234    pub fn new(config: LspConfig) -> Self {
235        Self {
236            manager: RwLock::new(None),
237            config,
238        }
239    }
240
241    pub async fn initialize(&self) -> Result<()> {
242        let mut manager = self.manager.write().await;
243        *manager = Some(LspManager::new(self.config.clone()).await?);
244        Ok(())
245    }
246
247    pub async fn search(&self, query: &str, file_path: Option<&str>) -> Result<LspSearchResponse> {
248        let manager = self.manager.read().await;
249        match manager.as_ref() {
250            Some(mgr) => mgr.search(query, file_path).await,
251            None => {
252                tracing::warn!("LSP not initialized, returning empty response");
253                Ok(LspSearchResponse {
254                    lsp_results: vec![],
255                    fallback_results: vec![],
256                    total_time_ms: 0,
257                    lsp_time_ms: 0,
258                    cache_hit_rate: 0.0,
259                    server_types_used: vec![],
260                    intent: QueryIntent::TextSearch,
261                })
262            }
263        }
264    }
265
266    pub async fn shutdown(&self) -> Result<()> {
267        let mut manager = self.manager.write().await;
268        if let Some(mut mgr) = manager.take() {
269            mgr.shutdown().await?;
270        }
271        Ok(())
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_server_type_from_extension() {
281        assert_eq!(LspServerType::from_file_extension("ts"), Some(LspServerType::TypeScript));
282        assert_eq!(LspServerType::from_file_extension("py"), Some(LspServerType::Python));
283        assert_eq!(LspServerType::from_file_extension("rs"), Some(LspServerType::Rust));
284        assert_eq!(LspServerType::from_file_extension("go"), Some(LspServerType::Go));
285        assert_eq!(LspServerType::from_file_extension("xyz"), None);
286    }
287
288    #[test]
289    fn test_query_intent_classification() {
290        assert_eq!(QueryIntent::classify("def myFunction"), QueryIntent::Definition);
291        assert_eq!(QueryIntent::classify("ref someVariable"), QueryIntent::References);
292        assert_eq!(QueryIntent::classify("type MyInterface"), QueryIntent::TypeDefinition);
293        assert_eq!(QueryIntent::classify("impl MyTrait"), QueryIntent::Implementation);
294        assert_eq!(QueryIntent::classify("@symbolName"), QueryIntent::Symbol);
295        assert_eq!(QueryIntent::classify("what is this?"), QueryIntent::Hover);
296        assert_eq!(QueryIntent::classify("random text"), QueryIntent::TextSearch);
297    }
298
299    #[test]
300    fn test_lsp_eligible_intent() {
301        assert!(QueryIntent::Definition.is_lsp_eligible());
302        assert!(QueryIntent::References.is_lsp_eligible());
303        assert!(QueryIntent::Symbol.is_lsp_eligible());
304        assert!(!QueryIntent::TextSearch.is_lsp_eligible());
305    }
306}