tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main widgets/text-editor-lsp/src/client.rs Rust · 501 líneas · 16.2 KB Historia Quién tocó qué crudo

feat: llimphi standalone — framework UI soberano extraído del monorepo e65e9cc62 · Sergio ·

1use super::*;
2
3pub struct RustAnalyzerClient {
4    /// Diagnostics activos por path. Lo escribe la task reader.
5    state: SharedState,
6    /// Sender al writer task. `None` si el spawn falló (modo no-op).
7    tx: Option<tokio::sync::mpsc::UnboundedSender<String>>,
8    /// Contador monotónico de request IDs.
9    next_id: i64,
10    /// Versiones por documento — el server las requiere en didChange.
11    versions: HashMap<PathBuf, i32>,
12    /// Runtime tokio dedicado — vive todo lo que viva el client.
13    /// `None` si el spawn falló.
14    _runtime: Option<Arc<tokio::runtime::Runtime>>,
15}
16
17impl RustAnalyzerClient {
18    /// Spawn `rust-analyzer` en `workspace_root`. Si el binary no está
19    /// en PATH, devuelve un client en modo no-op (sin error).
20    pub fn start(workspace_root: PathBuf) -> Self {
21        Self::with_command(workspace_root, "rust-analyzer")
22    }
23
24    /// Como `start` pero permite indicar el binary (`pylsp`, etc.).
25    pub fn with_command(workspace_root: PathBuf, command: &str) -> Self {
26        let state: SharedState = Arc::new(Mutex::new(SharedInner::default()));
27        let runtime = match tokio::runtime::Builder::new_multi_thread()
28            .worker_threads(2)
29            .enable_all()
30            .build()
31        {
32            Ok(rt) => Arc::new(rt),
33            Err(_) => {
34                return Self {
35                    state,
36                    tx: None,
37                    next_id: 1,
38                    versions: HashMap::new(),
39                    _runtime: None,
40                };
41            }
42        };
43
44        let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<String>();
45        let state_clone = state.clone();
46        let workspace_root_clone = workspace_root.clone();
47        let command_string = command.to_string();
48
49        runtime.spawn(async move {
50            if let Err(e) = run_server(workspace_root_clone, command_string, rx, state_clone).await
51            {
52                eprintln!("lsp: server task terminó con error: {e}");
53            }
54        });
55
56        let mut client = Self {
57            state,
58            tx: Some(tx),
59            next_id: 1,
60            versions: HashMap::new(),
61            _runtime: Some(runtime),
62        };
63        client.send_initialize(&workspace_root);
64        client
65    }
66
67    fn send_initialize(&mut self, root: &Path) {
68        let id = self.alloc_id();
69        let params = serde_json::json!({
70            "processId": std::process::id(),
71            "rootUri": format!("file://{}", root.display()),
72            "capabilities": {
73                "textDocument": {
74                    "publishDiagnostics": { "relatedInformation": false }
75                }
76            },
77            "clientInfo": { "name": "llimphi-text-editor-lsp", "version": "0.1.0" }
78        });
79        let req = serde_json::json!({
80            "jsonrpc": "2.0",
81            "id": id,
82            "method": "initialize",
83            "params": params
84        });
85        self.send_raw(req.to_string());
86        // El handshake termina con la notification `initialized` que
87        // mandamos sin esperar la response — el reader la procesará.
88        let notif = serde_json::json!({
89            "jsonrpc": "2.0",
90            "method": "initialized",
91            "params": {}
92        });
93        self.send_raw(notif.to_string());
94    }
95
96    fn alloc_id(&mut self) -> i64 {
97        let id = self.next_id;
98        self.next_id += 1;
99        id
100    }
101
102    fn send_raw(&self, msg: String) {
103        if let Some(tx) = &self.tx {
104            let _ = tx.send(msg);
105        }
106    }
107
108    fn lsp_language_id(language: &str) -> &str {
109        match language {
110            "rust" | "rs" => "rust",
111            "python" | "py" => "python",
112            other => other,
113        }
114    }
115}
116
117impl LspClient for RustAnalyzerClient {
118    fn diagnostics(&self, path: &Path) -> Vec<Diagnostic> {
119        self.state
120            .lock()
121            .ok()
122            .and_then(|s| s.diagnostics.get(path).cloned())
123            .unwrap_or_default()
124    }
125
126    fn request_completions(&mut self, path: &Path, line: usize, col: usize) {
127        let id = self.alloc_id();
128        if let Ok(mut s) = self.state.lock() {
129            s.pending_completion_ids.insert(id);
130        }
131        let req = serde_json::json!({
132            "jsonrpc": "2.0",
133            "id": id,
134            "method": "textDocument/completion",
135            "params": {
136                "textDocument": { "uri": format!("file://{}", path.display()) },
137                "position": { "line": line, "character": col }
138            }
139        });
140        self.send_raw(req.to_string());
141    }
142
143    fn latest_completions(&self) -> Vec<CompletionItem> {
144        self.state
145            .lock()
146            .map(|s| s.completions.clone())
147            .unwrap_or_default()
148    }
149
150    fn clear_completions(&mut self) {
151        if let Ok(mut s) = self.state.lock() {
152            s.completions.clear();
153        }
154    }
155
156    fn request_hover(&mut self, path: &Path, line: usize, col: usize) {
157        let id = self.alloc_id();
158        if let Ok(mut s) = self.state.lock() {
159            s.pending_hover_ids.insert(id);
160        }
161        let req = serde_json::json!({
162            "jsonrpc": "2.0",
163            "id": id,
164            "method": "textDocument/hover",
165            "params": {
166                "textDocument": { "uri": format!("file://{}", path.display()) },
167                "position": { "line": line, "character": col }
168            }
169        });
170        self.send_raw(req.to_string());
171    }
172
173    fn latest_hover(&self) -> Option<HoverInfo> {
174        self.state.lock().ok().and_then(|s| s.hover.clone())
175    }
176
177    fn clear_hover(&mut self) {
178        if let Ok(mut s) = self.state.lock() {
179            s.hover = None;
180        }
181    }
182
183    fn request_definition(&mut self, path: &Path, line: usize, col: usize) {
184        let id = self.alloc_id();
185        if let Ok(mut s) = self.state.lock() {
186            s.pending_definition_ids.insert(id);
187        }
188        let req = serde_json::json!({
189            "jsonrpc": "2.0",
190            "id": id,
191            "method": "textDocument/definition",
192            "params": {
193                "textDocument": { "uri": format!("file://{}", path.display()) },
194                "position": { "line": line, "character": col }
195            }
196        });
197        self.send_raw(req.to_string());
198    }
199
200    fn latest_definition(&self) -> Option<DefinitionLocation> {
201        self.state.lock().ok().and_then(|s| s.definition.clone())
202    }
203
204    fn clear_definition(&mut self) {
205        if let Ok(mut s) = self.state.lock() {
206            s.definition = None;
207        }
208    }
209
210    fn request_formatting(&mut self, path: &Path, tab_size: u32, insert_spaces: bool) {
211        let id = self.alloc_id();
212        if let Ok(mut s) = self.state.lock() {
213            s.pending_formatting_ids.insert(id);
214        }
215        let req = serde_json::json!({
216            "jsonrpc": "2.0",
217            "id": id,
218            "method": "textDocument/formatting",
219            "params": {
220                "textDocument": { "uri": format!("file://{}", path.display()) },
221                "options": {
222                    "tabSize": tab_size,
223                    "insertSpaces": insert_spaces
224                }
225            }
226        });
227        self.send_raw(req.to_string());
228    }
229
230    fn latest_text_edits(&self) -> Vec<TextEdit> {
231        self.state.lock().map(|s| s.text_edits.clone()).unwrap_or_default()
232    }
233
234    fn clear_text_edits(&mut self) {
235        if let Ok(mut s) = self.state.lock() {
236            s.text_edits.clear();
237        }
238    }
239
240    fn request_signature_help(&mut self, path: &Path, line: usize, col: usize) {
241        let id = self.alloc_id();
242        if let Ok(mut s) = self.state.lock() {
243            s.pending_signature_help_ids.insert(id);
244        }
245        let req = serde_json::json!({
246            "jsonrpc": "2.0",
247            "id": id,
248            "method": "textDocument/signatureHelp",
249            "params": {
250                "textDocument": { "uri": format!("file://{}", path.display()) },
251                "position": { "line": line, "character": col }
252            }
253        });
254        self.send_raw(req.to_string());
255    }
256
257    fn latest_signature_help(&self) -> Option<SignatureHelpInfo> {
258        self.state.lock().ok().and_then(|s| s.signature_help.clone())
259    }
260
261    fn clear_signature_help(&mut self) {
262        if let Ok(mut s) = self.state.lock() {
263            s.signature_help = None;
264        }
265    }
266
267    fn request_references(&mut self, path: &Path, line: usize, col: usize, include_decl: bool) {
268        let id = self.alloc_id();
269        if let Ok(mut s) = self.state.lock() {
270            s.pending_references_ids.insert(id);
271        }
272        let req = serde_json::json!({
273            "jsonrpc": "2.0",
274            "id": id,
275            "method": "textDocument/references",
276            "params": {
277                "textDocument": { "uri": format!("file://{}", path.display()) },
278                "position": { "line": line, "character": col },
279                "context": { "includeDeclaration": include_decl }
280            }
281        });
282        self.send_raw(req.to_string());
283    }
284
285    fn latest_references(&self) -> Vec<DefinitionLocation> {
286        self.state.lock().map(|s| s.references.clone()).unwrap_or_default()
287    }
288
289    fn clear_references(&mut self) {
290        if let Ok(mut s) = self.state.lock() {
291            s.references.clear();
292        }
293    }
294
295    fn request_rename(&mut self, path: &Path, line: usize, col: usize, new_name: &str) {
296        let id = self.alloc_id();
297        if let Ok(mut s) = self.state.lock() {
298            s.pending_rename_ids.insert(id);
299        }
300        let req = serde_json::json!({
301            "jsonrpc": "2.0",
302            "id": id,
303            "method": "textDocument/rename",
304            "params": {
305                "textDocument": { "uri": format!("file://{}", path.display()) },
306                "position": { "line": line, "character": col },
307                "newName": new_name
308            }
309        });
310        self.send_raw(req.to_string());
311    }
312
313    fn latest_workspace_edit(&self) -> std::collections::HashMap<PathBuf, Vec<TextEdit>> {
314        self.state.lock().map(|s| s.workspace_edit.clone()).unwrap_or_default()
315    }
316
317    fn clear_workspace_edit(&mut self) {
318        if let Ok(mut s) = self.state.lock() {
319            s.workspace_edit.clear();
320        }
321    }
322
323    fn request_document_symbols(&mut self, path: &Path) {
324        let id = self.alloc_id();
325        if let Ok(mut s) = self.state.lock() {
326            s.pending_document_symbols_ids.insert(id);
327        }
328        let req = serde_json::json!({
329            "jsonrpc": "2.0",
330            "id": id,
331            "method": "textDocument/documentSymbol",
332            "params": {
333                "textDocument": { "uri": format!("file://{}", path.display()) }
334            }
335        });
336        self.send_raw(req.to_string());
337    }
338
339    fn latest_document_symbols(&self) -> Vec<DocumentSymbolEntry> {
340        self.state.lock().map(|s| s.document_symbols.clone()).unwrap_or_default()
341    }
342
343    fn clear_document_symbols(&mut self) {
344        if let Ok(mut s) = self.state.lock() {
345            s.document_symbols.clear();
346        }
347    }
348
349    fn did_open(&mut self, path: &Path, language: &str, text: &str) {
350        self.versions.insert(path.to_path_buf(), 1);
351        let notif = serde_json::json!({
352            "jsonrpc": "2.0",
353            "method": "textDocument/didOpen",
354            "params": {
355                "textDocument": {
356                    "uri": format!("file://{}", path.display()),
357                    "languageId": Self::lsp_language_id(language),
358                    "version": 1,
359                    "text": text,
360                }
361            }
362        });
363        self.send_raw(notif.to_string());
364    }
365
366    fn did_change(&mut self, path: &Path, new_text: &str) {
367        let version = {
368            let v = self.versions.entry(path.to_path_buf()).or_insert(1);
369            *v += 1;
370            *v
371        };
372        // Full-document change. Más eficiente sería incremental, pero
373        // requiere trackear los EditDeltas del editor — futuro.
374        let notif = serde_json::json!({
375            "jsonrpc": "2.0",
376            "method": "textDocument/didChange",
377            "params": {
378                "textDocument": {
379                    "uri": format!("file://{}", path.display()),
380                    "version": version,
381                },
382                "contentChanges": [{ "text": new_text }]
383            }
384        });
385        self.send_raw(notif.to_string());
386    }
387
388    fn did_close(&mut self, path: &Path) {
389        self.versions.remove(path);
390        let notif = serde_json::json!({
391            "jsonrpc": "2.0",
392            "method": "textDocument/didClose",
393            "params": {
394                "textDocument": { "uri": format!("file://{}", path.display()) }
395            }
396        });
397        self.send_raw(notif.to_string());
398        if let Ok(mut s) = self.state.lock() {
399            s.diagnostics.remove(path);
400        }
401    }
402}
403
404// ---------------------------------------------------------------------
405// Task tokio que corre el server + bombea I/O
406// ---------------------------------------------------------------------
407
408async fn run_server(
409    _workspace_root: PathBuf,
410    command: String,
411    mut rx: tokio::sync::mpsc::UnboundedReceiver<String>,
412    state: SharedState,
413) -> std::io::Result<()> {
414    use std::process::Stdio;
415    use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
416    use tokio::process::Command;
417
418    let mut child = match Command::new(&command)
419        .stdin(Stdio::piped())
420        .stdout(Stdio::piped())
421        .stderr(Stdio::piped())
422        .spawn()
423    {
424        Ok(c) => c,
425        Err(e) => {
426            eprintln!("lsp: no pude spawn `{command}`: {e}");
427            return Ok(());
428        }
429    };
430
431    let stdin = child.stdin.take().expect("stdin piped");
432    let stdout = child.stdout.take().expect("stdout piped");
433
434    // Writer task: consume el rx y manda al stdin con headers LSP.
435    let writer = tokio::spawn(async move {
436        let mut stdin = stdin;
437        while let Some(msg) = rx.recv().await {
438            let header = format!("Content-Length: {}\r\n\r\n", msg.len());
439            if stdin.write_all(header.as_bytes()).await.is_err() {
440                break;
441            }
442            if stdin.write_all(msg.as_bytes()).await.is_err() {
443                break;
444            }
445            let _ = stdin.flush().await;
446        }
447    });
448
449    // Reader task: parsea mensajes del stdout, procesa publishDiagnostics.
450    let reader = tokio::spawn({
451        let state = state.clone();
452        async move {
453            let mut reader = BufReader::new(stdout);
454            loop {
455                let mut content_length: Option<usize> = None;
456                // Headers — terminan con línea vacía.
457                loop {
458                    let mut line = String::new();
459                    match reader.read_line(&mut line).await {
460                        Ok(0) => return, // EOF
461                        Ok(_) => {}
462                        Err(_) => return,
463                    }
464                    let line = line.trim_end_matches(['\r', '\n']);
465                    if line.is_empty() {
466                        break;
467                    }
468                    if let Some(rest) = line.strip_prefix("Content-Length:") {
469                        if let Ok(n) = rest.trim().parse::<usize>() {
470                            content_length = Some(n);
471                        }
472                    }
473                }
474                let Some(len) = content_length else { continue };
475                let mut buf = vec![0u8; len];
476                if reader.read_exact(&mut buf).await.is_err() {
477                    return;
478                }
479                let Ok(json) = serde_json::from_slice::<serde_json::Value>(&buf) else {
480                    continue;
481                };
482                if json.get("method").and_then(|m| m.as_str())
483                    == Some("textDocument/publishDiagnostics")
484                {
485                    handle_publish_diagnostics(&json, &state);
486                } else if let Some(id) = json.get("id").and_then(|i| i.as_i64()) {
487                    handle_response(id, &json, &state);
488                }
489            }
490        }
491    });
492
493    // Esperamos a que se cierre cualquiera de los dos lados o el child.
494    tokio::select! {
495        _ = writer => {}
496        _ = reader => {}
497        _ = child.wait() => {}
498    }
499    let _ = child.kill().await;
500    Ok(())
501}