dx_forge/server/
lsp.rs

1//! DX Forge Language Server
2//!
3//! LSP server for DX component detection, auto-completion, and semantic analysis
4
5use anyhow::Result;
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8use tokio::sync::RwLock;
9use tracing::info;
10
11use crate::patterns::PatternDetector;
12
13/// LSP Server state
14pub struct LspServer {
15    /// Pattern detector for DX components
16    pattern_detector: PatternDetector,
17    
18    /// Document store (uri -> content)
19    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    /// Handle document open
31    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        // Detect DX patterns
36        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    /// Handle document change
49    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    /// Handle document close
56    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    /// Provide completions for DX components
63    pub async fn completion(&self, uri: String, line: u32, character: u32) -> Result<Vec<CompletionItem>> {
64        info!("💡 Completion requested at {}:{}:{}", uri, line, character);
65        
66        // Get document text
67        let documents = self.documents.read().await;
68        let text = documents.get(&uri).map(|s| s.as_str()).unwrap_or("");
69        
70        // Get line text
71        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        // Provide DX completions if typing "dx"
80        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    /// Get DX component completions
88    fn get_dx_completions(&self) -> Vec<CompletionItem> {
89        vec![
90            // dx-ui components
91            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            // dx-icons
110            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    /// Provide hover information
126    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        // Detect DX patterns at position
131        let matches = self.pattern_detector.detect_in_file(
132            std::path::Path::new(&uri),
133            text
134        )?;
135        
136        // Find pattern at cursor position
137        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        // Should provide DX completions
201        assert!(!completions.is_empty());
202    }
203}