tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8
widgets/text-editor-lsp/src/client.rs
Historia
Quién tocó qué
crudo
feat: llimphi standalone — framework UI soberano extraído del monorepo
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}