tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8
widgets/text-editor-lsp/src/protocol.rs
Historia
Quién tocó qué
crudo
feat: llimphi standalone — framework UI soberano extraído del monorepo
1//! Parsers y handlers JSON-RPC de las respuestas/notificaciones LSP.
2
3use super::*;
4
5pub(crate) fn handle_publish_diagnostics(json: &serde_json::Value, state: &SharedState) {
6 let Some(params) = json.get("params") else { return };
7 let Some(uri) = params.get("uri").and_then(|u| u.as_str()) else { return };
8 let path = match uri.strip_prefix("file://") {
9 Some(p) => PathBuf::from(p),
10 None => return,
11 };
12 let Some(diags_arr) = params.get("diagnostics").and_then(|d| d.as_array()) else {
13 return;
14 };
15 let diagnostics: Vec<Diagnostic> = diags_arr
16 .iter()
17 .filter_map(parse_lsp_diagnostic)
18 .collect();
19 if let Ok(mut s) = state.lock() {
20 s.diagnostics.insert(path, diagnostics);
21 }
22}
23
24/// Routea una response del server al handler correspondiente según
25/// qué set de pendientes la contenía.
26pub(crate) fn handle_response(id: i64, json: &serde_json::Value, state: &SharedState) {
27 let flags = {
28 let Ok(mut s) = state.lock() else { return };
29 (
30 s.pending_completion_ids.remove(&id),
31 s.pending_hover_ids.remove(&id),
32 s.pending_definition_ids.remove(&id),
33 s.pending_formatting_ids.remove(&id),
34 s.pending_signature_help_ids.remove(&id),
35 s.pending_references_ids.remove(&id),
36 s.pending_rename_ids.remove(&id),
37 s.pending_document_symbols_ids.remove(&id),
38 )
39 };
40 let (was_completion, was_hover, was_def, was_fmt, was_sig, was_refs, was_rename, was_doc_sym) =
41 flags;
42 if was_completion {
43 handle_completion_response(json, state);
44 }
45 if was_hover {
46 handle_hover_response(json, state);
47 }
48 if was_def {
49 handle_definition_response(json, state);
50 }
51 if was_fmt {
52 handle_text_edits_response(json, state);
53 }
54 if was_sig {
55 handle_signature_help_response(json, state);
56 }
57 if was_refs {
58 handle_references_response(json, state);
59 }
60 if was_rename {
61 handle_rename_response(json, state);
62 }
63 if was_doc_sym {
64 handle_document_symbols_response(json, state);
65 }
66}
67
68/// Parsea la respuesta de `textDocument/documentSymbol`. Devuelve dos
69/// formatos posibles según la versión del server:
70///
71/// - `DocumentSymbol[]` (jerárquico, moderno) — el que usa rust-analyzer.
72/// - `SymbolInformation[]` (plano, legacy) — fallback razonable.
73///
74/// Ambos se flatten a `Vec<DocumentSymbolEntry>` con depth para que el
75/// caller pueda indentar visualmente.
76pub(crate) fn handle_document_symbols_response(json: &serde_json::Value, state: &SharedState) {
77 let Some(result) = json.get("result") else { return };
78 if result.is_null() {
79 if let Ok(mut s) = state.lock() {
80 s.document_symbols.clear();
81 }
82 return;
83 }
84 let mut out: Vec<DocumentSymbolEntry> = Vec::new();
85 if let Some(arr) = result.as_array() {
86 for item in arr {
87 // Distingue por la presencia de "selectionRange" (sólo en
88 // DocumentSymbol). SymbolInformation tiene "location" en
89 // su lugar.
90 if item.get("selectionRange").is_some() {
91 flatten_document_symbol(item, None, 0, &mut out);
92 } else if item.get("location").is_some() {
93 if let Some(entry) = parse_symbol_information(item) {
94 out.push(entry);
95 }
96 }
97 }
98 }
99 if let Ok(mut s) = state.lock() {
100 s.document_symbols = out;
101 }
102}
103
104/// Flatten recursivo de `DocumentSymbol`. `parent` es el nombre del
105/// contenedor (para que `container` quede poblado en métodos/campos).
106pub(crate) fn flatten_document_symbol(
107 node: &serde_json::Value,
108 parent: Option<&str>,
109 depth: u32,
110 out: &mut Vec<DocumentSymbolEntry>,
111) {
112 let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("?").to_string();
113 let kind_num = node.get("kind").and_then(|v| v.as_u64()).unwrap_or(0);
114 let kind = symbol_kind_label(kind_num);
115 // `selectionRange.start` es la pos del identificador (lo que el
116 // usuario quiere ver al saltar). `range.start` apuntaría al `{` de
117 // la definición — menos útil para outline.
118 let (line, col) = node
119 .get("selectionRange")
120 .and_then(|r| r.get("start"))
121 .and_then(parse_position)
122 .or_else(|| node.get("range").and_then(|r| r.get("start")).and_then(parse_position))
123 .unwrap_or((0, 0));
124 out.push(DocumentSymbolEntry {
125 name: name.clone(),
126 kind,
127 line,
128 col,
129 container: parent.map(|s| s.to_string()),
130 depth,
131 });
132 if let Some(children) = node.get("children").and_then(|c| c.as_array()) {
133 for child in children {
134 flatten_document_symbol(child, Some(&name), depth + 1, out);
135 }
136 }
137}
138
139pub(crate) fn parse_symbol_information(item: &serde_json::Value) -> Option<DocumentSymbolEntry> {
140 let name = item.get("name")?.as_str()?.to_string();
141 let kind_num = item.get("kind").and_then(|v| v.as_u64()).unwrap_or(0);
142 let location = item.get("location")?;
143 let (line, col) = location.get("range").and_then(|r| r.get("start")).and_then(parse_position)?;
144 let container = item
145 .get("containerName")
146 .and_then(|v| v.as_str())
147 .filter(|s| !s.is_empty())
148 .map(|s| s.to_string());
149 Some(DocumentSymbolEntry {
150 name,
151 kind: symbol_kind_label(kind_num),
152 line,
153 col,
154 container,
155 depth: 0,
156 })
157}
158
159pub(crate) fn parse_position(p: &serde_json::Value) -> Option<(usize, usize)> {
160 let line = p.get("line")?.as_u64()? as usize;
161 let col = p.get("character")?.as_u64()? as usize;
162 Some((line, col))
163}
164
165/// Mapea el `SymbolKind` numérico del LSP a la etiqueta corta que el
166/// outline pinta. Sólo cubre las que el usuario suele ver — el resto
167/// va a `"sym"`. Lista canónica: <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind>
168pub(crate) fn symbol_kind_label(kind: u64) -> String {
169 match kind {
170 2 => "mod",
171 5 => "class",
172 6 => "method",
173 7 => "property",
174 8 => "field",
175 9 => "ctor",
176 10 => "enum",
177 11 => "iface",
178 12 => "fn",
179 13 => "var",
180 14 => "const",
181 15 => "str",
182 18 => "arr",
183 22 => "variant",
184 23 => "struct",
185 26 => "type",
186 _ => "sym",
187 }
188 .to_string()
189}
190
191pub(crate) fn handle_rename_response(json: &serde_json::Value, state: &SharedState) {
192 let Some(result) = json.get("result") else { return };
193 if result.is_null() {
194 return;
195 }
196 let mut map: HashMap<PathBuf, Vec<TextEdit>> = HashMap::new();
197 // changes: { uri → TextEdit[] }
198 if let Some(changes) = result.get("changes").and_then(|c| c.as_object()) {
199 for (uri, edits_val) in changes {
200 let Some(path) = uri.strip_prefix("file://").map(PathBuf::from) else { continue };
201 let Some(arr) = edits_val.as_array() else { continue };
202 let edits: Vec<TextEdit> = arr.iter().filter_map(parse_text_edit).collect();
203 map.insert(path, edits);
204 }
205 }
206 // documentChanges: [{ textDocument: { uri }, edits: [...] }] — más nuevo.
207 if let Some(docs) = result.get("documentChanges").and_then(|c| c.as_array()) {
208 for doc in docs {
209 let Some(uri) = doc
210 .get("textDocument")
211 .and_then(|t| t.get("uri"))
212 .and_then(|u| u.as_str())
213 else {
214 continue;
215 };
216 let Some(path) = uri.strip_prefix("file://").map(PathBuf::from) else { continue };
217 let Some(arr) = doc.get("edits").and_then(|e| e.as_array()) else { continue };
218 let edits: Vec<TextEdit> = arr.iter().filter_map(parse_text_edit).collect();
219 map.entry(path).or_default().extend(edits);
220 }
221 }
222 if let Ok(mut s) = state.lock() {
223 s.workspace_edit = map;
224 }
225}
226
227pub(crate) fn handle_references_response(json: &serde_json::Value, state: &SharedState) {
228 let Some(result) = json.get("result") else { return };
229 if result.is_null() {
230 if let Ok(mut s) = state.lock() {
231 s.references.clear();
232 }
233 return;
234 }
235 let Some(arr) = result.as_array() else { return };
236 let refs: Vec<DefinitionLocation> = arr.iter().filter_map(parse_location).collect();
237 if let Ok(mut s) = state.lock() {
238 s.references = refs;
239 }
240}
241
242/// Parsea una `Location` LSP: { uri, range } → DefinitionLocation.
243pub(crate) fn parse_location(loc: &serde_json::Value) -> Option<DefinitionLocation> {
244 let uri = loc.get("uri")?.as_str()?;
245 let path = uri.strip_prefix("file://").map(PathBuf::from)?;
246 let range = loc.get("range")?;
247 let start = range.get("start")?;
248 let line = start.get("line")?.as_u64()? as usize;
249 let col = start.get("character")?.as_u64()? as usize;
250 Some(DefinitionLocation { path, line, col })
251}
252
253pub(crate) fn handle_signature_help_response(json: &serde_json::Value, state: &SharedState) {
254 let Some(result) = json.get("result") else { return };
255 if result.is_null() {
256 if let Ok(mut s) = state.lock() {
257 s.signature_help = None;
258 }
259 return;
260 }
261 let info = parse_signature_help(result);
262 if let Ok(mut s) = state.lock() {
263 s.signature_help = info;
264 }
265}
266
267pub(crate) fn parse_signature_help(result: &serde_json::Value) -> Option<SignatureHelpInfo> {
268 let sigs = result.get("signatures")?.as_array()?;
269 if sigs.is_empty() {
270 return None;
271 }
272 let active_sig = result.get("activeSignature").and_then(|n| n.as_u64()).unwrap_or(0) as usize;
273 let sig = sigs.get(active_sig).or_else(|| sigs.first())?;
274 let label = sig.get("label")?.as_str()?.to_string();
275 let doc = sig
276 .get("documentation")
277 .map(stringify_hover_contents)
278 .filter(|s| !s.is_empty());
279 let active_param = sig
280 .get("activeParameter")
281 .or_else(|| result.get("activeParameter"))
282 .and_then(|n| n.as_u64())
283 .unwrap_or(0) as usize;
284 let param_labels = sig
285 .get("parameters")
286 .and_then(|p| p.as_array())
287 .map(|arr| {
288 arr.iter()
289 .filter_map(|p| {
290 let lbl = p.get("label")?;
291 if let Some(s) = lbl.as_str() {
292 Some(s.to_string())
293 } else if let Some(arr2) = lbl.as_array() {
294 let s = arr2.first()?.as_u64()? as usize;
295 let e = arr2.get(1)?.as_u64()? as usize;
296 label.get(s..e).map(String::from)
297 } else {
298 None
299 }
300 })
301 .collect()
302 })
303 .unwrap_or_default();
304 Some(SignatureHelpInfo { label, doc, active_param, param_labels })
305}
306
307pub(crate) fn handle_text_edits_response(json: &serde_json::Value, state: &SharedState) {
308 let Some(result) = json.get("result") else { return };
309 if result.is_null() {
310 return;
311 }
312 let Some(arr) = result.as_array() else { return };
313 let edits: Vec<TextEdit> = arr.iter().filter_map(parse_text_edit).collect();
314 if let Ok(mut s) = state.lock() {
315 s.text_edits = edits;
316 }
317}
318
319pub(crate) fn parse_text_edit(v: &serde_json::Value) -> Option<TextEdit> {
320 let range = v.get("range")?;
321 let start = range.get("start")?;
322 let end = range.get("end")?;
323 let start_line = start.get("line")?.as_u64()? as usize;
324 let start_col = start.get("character")?.as_u64()? as usize;
325 let end_line = end.get("line")?.as_u64()? as usize;
326 let end_col = end.get("character")?.as_u64()? as usize;
327 let new_text = v.get("newText")?.as_str()?.to_string();
328 Some(TextEdit { start_line, start_col, end_line, end_col, new_text })
329}
330
331pub(crate) fn handle_definition_response(json: &serde_json::Value, state: &SharedState) {
332 let Some(result) = json.get("result") else { return };
333 if result.is_null() {
334 return;
335 }
336 // `result` puede ser:
337 // - Location { uri, range }
338 // - Location[]
339 // - LocationLink[] { targetUri, targetSelectionRange }
340 // Tomamos la primera location en cualquier caso.
341 let loc_value = if result.is_array() {
342 result.as_array().and_then(|a| a.first()).cloned()
343 } else {
344 Some(result.clone())
345 };
346 let Some(loc) = loc_value else { return };
347
348 let (uri, range) = if let Some(u) = loc.get("uri") {
349 (u, loc.get("range"))
350 } else if let Some(u) = loc.get("targetUri") {
351 (
352 u,
353 loc.get("targetSelectionRange").or_else(|| loc.get("targetRange")),
354 )
355 } else {
356 return;
357 };
358 let Some(uri) = uri.as_str() else { return };
359 let path = match uri.strip_prefix("file://") {
360 Some(p) => PathBuf::from(p),
361 None => return,
362 };
363 let Some(range) = range else { return };
364 let Some(start) = range.get("start") else { return };
365 let line = start.get("line").and_then(|n| n.as_u64()).unwrap_or(0) as usize;
366 let col = start.get("character").and_then(|n| n.as_u64()).unwrap_or(0) as usize;
367 if let Ok(mut s) = state.lock() {
368 s.definition = Some(DefinitionLocation { path, line, col });
369 }
370}
371
372pub(crate) fn handle_completion_response(json: &serde_json::Value, state: &SharedState) {
373 let Some(result) = json.get("result") else { return };
374 let items_arr = if let Some(arr) = result.as_array() {
375 arr.clone()
376 } else if let Some(items) = result.get("items").and_then(|i| i.as_array()) {
377 items.clone()
378 } else {
379 return;
380 };
381 let completions: Vec<CompletionItem> = items_arr.iter().filter_map(parse_completion).collect();
382 if let Ok(mut s) = state.lock() {
383 s.completions = completions;
384 }
385}
386
387pub(crate) fn handle_hover_response(json: &serde_json::Value, state: &SharedState) {
388 let Some(result) = json.get("result") else { return };
389 if result.is_null() {
390 if let Ok(mut s) = state.lock() {
391 s.hover = None;
392 }
393 return;
394 }
395 let info = parse_hover(result);
396 if let Ok(mut s) = state.lock() {
397 s.hover = info;
398 }
399}
400
401/// `contents` en LSP puede ser:
402/// - String
403/// - { kind: "markdown"|"plaintext", value: String }
404/// - Array de los anteriores (deprecated pero algunos servers lo mandan)
405/// - { language: ..., value: ... } (legacy MarkedString)
406pub(crate) fn parse_hover(result: &serde_json::Value) -> Option<HoverInfo> {
407 let contents = result.get("contents")?;
408 let text = stringify_hover_contents(contents);
409 if text.is_empty() {
410 None
411 } else {
412 Some(HoverInfo { contents: text })
413 }
414}
415
416pub(crate) fn stringify_hover_contents(v: &serde_json::Value) -> String {
417 match v {
418 serde_json::Value::String(s) => s.clone(),
419 serde_json::Value::Object(map) => {
420 // { kind, value } o { language, value }
421 map.get("value")
422 .and_then(|s| s.as_str())
423 .unwrap_or("")
424 .to_string()
425 }
426 serde_json::Value::Array(arr) => arr
427 .iter()
428 .map(stringify_hover_contents)
429 .filter(|s| !s.is_empty())
430 .collect::<Vec<_>>()
431 .join("\n"),
432 _ => String::new(),
433 }
434}
435
436pub(crate) fn parse_completion(v: &serde_json::Value) -> Option<CompletionItem> {
437 let label = v.get("label")?.as_str()?.to_string();
438 let insert_text = v
439 .get("insertText")
440 .and_then(|s| s.as_str())
441 .map(String::from);
442 let kind = v
443 .get("kind")
444 .and_then(|k| k.as_u64())
445 .map(|n| completion_kind_label(n).to_string());
446 let detail = v
447 .get("detail")
448 .and_then(|d| d.as_str())
449 .map(String::from);
450 Some(CompletionItem { label, insert_text, kind, detail })
451}
452
453/// Etiqueta corta para el CompletionItemKind de LSP (1..25).
454pub(crate) fn completion_kind_label(k: u64) -> &'static str {
455 match k {
456 1 => "Text",
457 2 => "Method",
458 3 => "Function",
459 4 => "Ctor",
460 5 => "Field",
461 6 => "Var",
462 7 => "Class",
463 8 => "Iface",
464 9 => "Mod",
465 10 => "Prop",
466 11 => "Unit",
467 12 => "Value",
468 13 => "Enum",
469 14 => "Keyword",
470 15 => "Snip",
471 16 => "Color",
472 17 => "File",
473 18 => "Ref",
474 19 => "Folder",
475 20 => "EnumMember",
476 21 => "Const",
477 22 => "Struct",
478 23 => "Event",
479 24 => "Op",
480 25 => "TypeParam",
481 _ => "?",
482 }
483}
484
485pub(crate) fn parse_lsp_diagnostic(d: &serde_json::Value) -> Option<Diagnostic> {
486 let range = d.get("range")?;
487 let start = range.get("start")?;
488 let end = range.get("end")?;
489 let sl = start.get("line")?.as_u64()? as usize;
490 let sc = start.get("character")?.as_u64()? as usize;
491 let el = end.get("line")?.as_u64()? as usize;
492 let ec = end.get("character")?.as_u64()? as usize;
493 let severity = match d.get("severity").and_then(|s| s.as_u64()) {
494 Some(1) => Severity::Error,
495 Some(2) => Severity::Warning,
496 Some(3) => Severity::Information,
497 Some(4) => Severity::Hint,
498 _ => Severity::Information,
499 };
500 let message = d
501 .get("message")
502 .and_then(|m| m.as_str())
503 .unwrap_or("")
504 .to_string();
505 let source = d.get("source").and_then(|s| s.as_str()).map(String::from);
506 Some(Diagnostic {
507 range: DiagnosticRange {
508 start: Pos::new(sl, sc),
509 end: Pos::new(el, ec),
510 },
511 severity,
512 message,
513 source,
514 })
515}