tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Sutichaykuna

⎇ main widgets/text-editor-lsp/src/protocol.rs Rust · 515 siq'ikuna · 17.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

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}