1pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
36pub enum LspServerType {
37 TypeScript, Python, Rust, Go, JavaScript, }
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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub enum QueryIntent {
79 Definition,
81 References,
83 TypeDefinition,
85 Implementation,
87 Declaration,
89 Symbol,
91 Completion,
93 Hover,
95 TextSearch,
97}
98
99impl QueryIntent {
100 pub fn classify(query: &str) -> Self {
102 let query_lower = query.to_lowercase();
103
104 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 pub fn is_lsp_eligible(&self) -> bool {
124 !matches!(self, Self::TextSearch)
125 }
126
127 pub fn requires_safety_floor(&self) -> bool {
132 matches!(self, Self::Definition | Self::Symbol | Self::TypeDefinition | Self::Implementation)
133 }
134
135 pub fn is_exact_query(&self) -> bool {
137 matches!(self, Self::Definition | Self::Symbol)
138 }
139
140 pub fn is_structural_query(&self) -> bool {
142 matches!(self, Self::TypeDefinition | Self::Implementation)
143 }
144}
145
146#[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, max_results: 64, timeout_ms: 5000, }
161 }
162}
163
164#[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#[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#[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, 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, traversal_bounds: TraversalBounds::default(),
223 }
224 }
225}
226
227pub 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}