1use anyhow::Result;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use tokio::sync::RwLock;
9use tracing::info;
10
11use crate::patterns::PatternDetector;
12
13pub struct LspServer {
15 pattern_detector: PatternDetector,
17
18 documents: Arc<RwLock<std::collections::HashMap<String, String>>>,
20}
21
22impl LspServer {
23 pub fn new() -> Result<Self> {
24 Ok(Self {
25 pattern_detector: PatternDetector::new()?,
26 documents: Arc::new(RwLock::new(std::collections::HashMap::new())),
27 })
28 }
29
30 pub async fn did_open(&self, uri: String, text: String) -> Result<()> {
32 info!("📄 Document opened: {}", uri);
33 self.documents.write().await.insert(uri.clone(), text.clone());
34
35 let matches = self.pattern_detector.detect_in_file(
37 std::path::Path::new(&uri),
38 &text
39 )?;
40
41 if !matches.is_empty() {
42 info!("🔍 Found {} DX patterns in {}", matches.len(), uri);
43 }
44
45 Ok(())
46 }
47
48 pub async fn did_change(&self, uri: String, text: String) -> Result<()> {
50 info!("✏️ Document changed: {}", uri);
51 self.documents.write().await.insert(uri, text);
52 Ok(())
53 }
54
55 pub async fn did_close(&self, uri: String) -> Result<()> {
57 info!("📪 Document closed: {}", uri);
58 self.documents.write().await.remove(&uri);
59 Ok(())
60 }
61
62 pub async fn completion(&self, uri: String, line: u32, character: u32) -> Result<Vec<CompletionItem>> {
64 info!("💡 Completion requested at {}:{}:{}", uri, line, character);
65
66 let documents = self.documents.read().await;
68 let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
69
70 let lines: Vec<&str> = text.lines().collect();
72 if line as usize >= lines.len() {
73 return Ok(Vec::new());
74 }
75
76 let line_text = lines[line as usize];
77 let prefix = &line_text[..character.min(line_text.len() as u32) as usize];
78
79 if prefix.ends_with("dx") || prefix.ends_with("<dx") {
81 Ok(self.get_dx_completions())
82 } else {
83 Ok(Vec::new())
84 }
85 }
86
87 fn get_dx_completions(&self) -> Vec<CompletionItem> {
89 vec![
90 CompletionItem {
92 label: "dxButton".to_string(),
93 kind: CompletionItemKind::Component,
94 detail: Some("DX UI Button component".to_string()),
95 documentation: Some("Auto-injected button component from dx-ui".to_string()),
96 },
97 CompletionItem {
98 label: "dxInput".to_string(),
99 kind: CompletionItemKind::Component,
100 detail: Some("DX UI Input component".to_string()),
101 documentation: Some("Auto-injected input component from dx-ui".to_string()),
102 },
103 CompletionItem {
104 label: "dxCard".to_string(),
105 kind: CompletionItemKind::Component,
106 detail: Some("DX UI Card component".to_string()),
107 documentation: Some("Auto-injected card component from dx-ui".to_string()),
108 },
109 CompletionItem {
111 label: "dxiHome".to_string(),
112 kind: CompletionItemKind::Component,
113 detail: Some("DX Icon: Home".to_string()),
114 documentation: Some("Auto-injected home icon from dx-icons".to_string()),
115 },
116 CompletionItem {
117 label: "dxiUser".to_string(),
118 kind: CompletionItemKind::Component,
119 detail: Some("DX Icon: User".to_string(),),
120 documentation: Some("Auto-injected user icon from dx-icons".to_string()),
121 },
122 ]
123 }
124
125 pub async fn hover(&self, uri: String, line: u32, _character: u32) -> Result<Option<HoverInfo>> {
127 let documents = self.documents.read().await;
128 let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
129
130 let matches = self.pattern_detector.detect_in_file(
132 std::path::Path::new(&uri),
133 text
134 )?;
135
136 for m in matches {
138 if m.line == (line + 1) as usize {
139 let info = HoverInfo {
140 contents: format!(
141 "**{}** from {}\n\nAuto-injected DX component",
142 m.component_name,
143 m.tool.tool_name()
144 ),
145 };
146 return Ok(Some(info));
147 }
148 }
149
150 Ok(None)
151 }
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct CompletionItem {
156 pub label: String,
157 pub kind: CompletionItemKind,
158 pub detail: Option<String>,
159 pub documentation: Option<String>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub enum CompletionItemKind {
164 Component,
165 Function,
166 Variable,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct HoverInfo {
171 pub contents: String,
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[tokio::test]
179async fn test_lsp_server_creation() {
180 let server = LspServer::new().unwrap();
181 assert!(server.documents.read().await.is_empty());
182 }
183
184 #[tokio::test]
185 async fn test_did_open() {
186 let server = LspServer::new().unwrap();
187 let content = "<dxButton>Click me</dxButton>";
188
189 server.did_open("test.tsx".to_string(), content.to_string()).await.unwrap();
190
191 let docs = server.documents.read().await;
192 assert_eq!(docs.get("test.tsx"), Some(&content.to_string()));
193 }
194
195 #[tokio::test]
196 async fn test_completion() {
197 let server = LspServer::new().unwrap();
198 let completions = server.completion("test.tsx".to_string(), 0, 2).await.unwrap();
199
200 assert!(!completions.is_empty());
202 }
203}