use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::info;
use crate::patterns::PatternDetector;
pub struct LspServer {
pattern_detector: PatternDetector,
documents: Arc<RwLock<std::collections::HashMap<String, String>>>,
}
impl LspServer {
pub fn new() -> Result<Self> {
Ok(Self {
pattern_detector: PatternDetector::new()?,
documents: Arc::new(RwLock::new(std::collections::HashMap::new())),
})
}
pub async fn did_open(&self, uri: String, text: String) -> Result<()> {
info!("📄 Document opened: {}", uri);
self.documents.write().await.insert(uri.clone(), text.clone());
let matches = self.pattern_detector.detect_in_file(
std::path::Path::new(&uri),
&text
)?;
if !matches.is_empty() {
info!("🔍 Found {} DX patterns in {}", matches.len(), uri);
}
Ok(())
}
pub async fn did_change(&self, uri: String, text: String) -> Result<()> {
info!("✏️ Document changed: {}", uri);
self.documents.write().await.insert(uri, text);
Ok(())
}
pub async fn did_close(&self, uri: String) -> Result<()> {
info!("📪 Document closed: {}", uri);
self.documents.write().await.remove(&uri);
Ok(())
}
pub async fn completion(&self, uri: String, line: u32, character: u32) -> Result<Vec<CompletionItem>> {
info!("💡 Completion requested at {}:{}:{}", uri, line, character);
let documents = self.documents.read().await;
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
let lines: Vec<&str> = text.lines().collect();
if line as usize >= lines.len() {
return Ok(Vec::new());
}
let line_text = lines[line as usize];
let prefix = &line_text[..character.min(line_text.len() as u32) as usize];
if prefix.ends_with("dx") || prefix.ends_with("<dx") {
Ok(self.get_dx_completions())
} else {
Ok(Vec::new())
}
}
fn get_dx_completions(&self) -> Vec<CompletionItem> {
vec![
CompletionItem {
label: "dxButton".to_string(),
kind: CompletionItemKind::Component,
detail: Some("DX UI Button component".to_string()),
documentation: Some("Auto-injected button component from dx-ui".to_string()),
},
CompletionItem {
label: "dxInput".to_string(),
kind: CompletionItemKind::Component,
detail: Some("DX UI Input component".to_string()),
documentation: Some("Auto-injected input component from dx-ui".to_string()),
},
CompletionItem {
label: "dxCard".to_string(),
kind: CompletionItemKind::Component,
detail: Some("DX UI Card component".to_string()),
documentation: Some("Auto-injected card component from dx-ui".to_string()),
},
CompletionItem {
label: "dxiHome".to_string(),
kind: CompletionItemKind::Component,
detail: Some("DX Icon: Home".to_string()),
documentation: Some("Auto-injected home icon from dx-icons".to_string()),
},
CompletionItem {
label: "dxiUser".to_string(),
kind: CompletionItemKind::Component,
detail: Some("DX Icon: User".to_string(),),
documentation: Some("Auto-injected user icon from dx-icons".to_string()),
},
]
}
pub async fn hover(&self, uri: String, line: u32, _character: u32) -> Result<Option<HoverInfo>> {
let documents = self.documents.read().await;
let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
let matches = self.pattern_detector.detect_in_file(
std::path::Path::new(&uri),
text
)?;
for m in matches {
if m.line == (line + 1) as usize {
let info = HoverInfo {
contents: format!(
"**{}** from {}\n\nAuto-injected DX component",
m.component_name,
m.tool.tool_name()
),
};
return Ok(Some(info));
}
}
Ok(None)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompletionItem {
pub label: String,
pub kind: CompletionItemKind,
pub detail: Option<String>,
pub documentation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CompletionItemKind {
Component,
Function,
Variable,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HoverInfo {
pub contents: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_lsp_server_creation() {
let server = LspServer::new().unwrap();
assert!(server.documents.read().await.is_empty());
}
#[tokio::test]
async fn test_did_open() {
let server = LspServer::new().unwrap();
let content = "<dxButton>Click me</dxButton>";
server.did_open("test.tsx".to_string(), content.to_string()).await.unwrap();
let docs = server.documents.read().await;
assert_eq!(docs.get("test.tsx"), Some(&content.to_string()));
}
#[tokio::test]
async fn test_completion() {
let server = LspServer::new().unwrap();
let completions = server.completion("test.tsx".to_string(), 0, 2).await.unwrap();
assert!(!completions.is_empty());
}
}