pub mod client;
pub mod hint;
pub mod manager;
pub mod router;
pub mod server_process;
use anyhow::Result;
use lsp_types::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tokio::sync::RwLock;
pub use client::LspClient;
pub use hint::{HintCache, SymbolHint, HintType};
pub use manager::LspManager;
pub use router::LspRouter;
pub use server_process::LspServerProcess;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LspServerType {
TypeScript, Python, Rust, Go, JavaScript, }
impl LspServerType {
pub fn from_file_extension(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"ts" | "tsx" => Some(Self::TypeScript),
"js" | "jsx" => Some(Self::JavaScript),
"py" | "pyi" => Some(Self::Python),
"rs" => Some(Self::Rust),
"go" => Some(Self::Go),
_ => None,
}
}
pub fn server_command(&self) -> (&'static str, Vec<&'static str>) {
match self {
Self::TypeScript | Self::JavaScript => ("typescript-language-server", vec!["--stdio"]),
Self::Python => ("pylsp", vec!["--verbose"]),
Self::Rust => ("rust-analyzer", vec![]),
Self::Go => ("gopls", vec!["serve"]),
}
}
pub fn file_extensions(&self) -> Vec<&'static str> {
match self {
Self::TypeScript => vec!["ts", "tsx"],
Self::JavaScript => vec!["js", "jsx"],
Self::Python => vec!["py", "pyi"],
Self::Rust => vec!["rs"],
Self::Go => vec!["go"],
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum QueryIntent {
Definition,
References,
TypeDefinition,
Implementation,
Declaration,
Symbol,
Completion,
Hover,
TextSearch,
}
impl QueryIntent {
pub fn classify(query: &str) -> Self {
let query_lower = query.to_lowercase();
if query_lower.contains("def ") || query_lower.contains("function ") || query_lower.contains("class ") {
Self::Definition
} else if query_lower.contains("ref ") || query_lower.contains("usage") || query_lower.contains("usages") {
Self::References
} else if query_lower.contains("type ") || query_lower.contains("interface ") {
Self::TypeDefinition
} else if query_lower.contains("impl ") || query_lower.contains("implement") {
Self::Implementation
} else if query_lower.starts_with("@") {
Self::Symbol
} else if query_lower.ends_with("?") {
Self::Hover
} else {
Self::TextSearch
}
}
pub fn is_lsp_eligible(&self) -> bool {
!matches!(self, Self::TextSearch)
}
pub fn requires_safety_floor(&self) -> bool {
matches!(self, Self::Definition | Self::Symbol | Self::TypeDefinition | Self::Implementation)
}
pub fn is_exact_query(&self) -> bool {
matches!(self, Self::Definition | Self::Symbol)
}
pub fn is_structural_query(&self) -> bool {
matches!(self, Self::TypeDefinition | Self::Implementation)
}
}
#[derive(Debug, Clone)]
pub struct TraversalBounds {
pub max_depth: u8,
pub max_results: u16,
pub timeout_ms: u64,
}
impl Default for TraversalBounds {
fn default() -> Self {
Self {
max_depth: 2, max_results: 64, timeout_ms: 5000, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LspSearchResult {
pub file_path: String,
pub line_number: u32,
pub column: u32,
pub content: String,
pub hint_type: HintType,
pub server_type: LspServerType,
pub confidence: f64,
pub context_lines: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LspSearchResponse {
pub lsp_results: Vec<LspSearchResult>,
pub fallback_results: Vec<crate::search::SearchResult>,
pub total_time_ms: u64,
pub lsp_time_ms: u64,
pub cache_hit_rate: f64,
pub server_types_used: Vec<LspServerType>,
pub intent: QueryIntent,
}
impl Default for LspSearchResponse {
fn default() -> Self {
Self {
lsp_results: Vec::new(),
fallback_results: Vec::new(),
total_time_ms: 0,
lsp_time_ms: 0,
cache_hit_rate: 0.0,
server_types_used: Vec::new(),
intent: QueryIntent::TextSearch,
}
}
}
#[derive(Debug, Clone)]
pub struct LspConfig {
pub enabled: bool,
pub server_timeout_ms: u64,
pub cache_ttl_hours: u64,
pub max_concurrent_requests: usize,
pub routing_percentage: f64, pub traversal_bounds: TraversalBounds,
}
impl Default for LspConfig {
fn default() -> Self {
Self {
enabled: true,
server_timeout_ms: 5000,
cache_ttl_hours: 24,
max_concurrent_requests: 10,
routing_percentage: 0.5, traversal_bounds: TraversalBounds::default(),
}
}
}
pub struct LspState {
manager: RwLock<Option<LspManager>>,
config: LspConfig,
}
impl LspState {
pub fn new(config: LspConfig) -> Self {
Self {
manager: RwLock::new(None),
config,
}
}
pub async fn initialize(&self) -> Result<()> {
let mut manager = self.manager.write().await;
*manager = Some(LspManager::new(self.config.clone()).await?);
Ok(())
}
pub async fn search(&self, query: &str, file_path: Option<&str>) -> Result<LspSearchResponse> {
let manager = self.manager.read().await;
match manager.as_ref() {
Some(mgr) => mgr.search(query, file_path).await,
None => {
tracing::warn!("LSP not initialized, returning empty response");
Ok(LspSearchResponse {
lsp_results: vec![],
fallback_results: vec![],
total_time_ms: 0,
lsp_time_ms: 0,
cache_hit_rate: 0.0,
server_types_used: vec![],
intent: QueryIntent::TextSearch,
})
}
}
}
pub async fn shutdown(&self) -> Result<()> {
let mut manager = self.manager.write().await;
if let Some(mut mgr) = manager.take() {
mgr.shutdown().await?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_type_from_extension() {
assert_eq!(LspServerType::from_file_extension("ts"), Some(LspServerType::TypeScript));
assert_eq!(LspServerType::from_file_extension("py"), Some(LspServerType::Python));
assert_eq!(LspServerType::from_file_extension("rs"), Some(LspServerType::Rust));
assert_eq!(LspServerType::from_file_extension("go"), Some(LspServerType::Go));
assert_eq!(LspServerType::from_file_extension("xyz"), None);
}
#[test]
fn test_query_intent_classification() {
assert_eq!(QueryIntent::classify("def myFunction"), QueryIntent::Definition);
assert_eq!(QueryIntent::classify("ref someVariable"), QueryIntent::References);
assert_eq!(QueryIntent::classify("type MyInterface"), QueryIntent::TypeDefinition);
assert_eq!(QueryIntent::classify("impl MyTrait"), QueryIntent::Implementation);
assert_eq!(QueryIntent::classify("@symbolName"), QueryIntent::Symbol);
assert_eq!(QueryIntent::classify("what is this?"), QueryIntent::Hover);
assert_eq!(QueryIntent::classify("random text"), QueryIntent::TextSearch);
}
#[test]
fn test_lsp_eligible_intent() {
assert!(QueryIntent::Definition.is_lsp_eligible());
assert!(QueryIntent::References.is_lsp_eligible());
assert!(QueryIntent::Symbol.is_lsp_eligible());
assert!(!QueryIntent::TextSearch.is_lsp_eligible());
}
}