e65e9cc623
Motor gráfico Llimphi como workspace independiente: bucle Elm (input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley. Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets + módulos, sin dependencias al resto del monorepo. cargo check --workspace pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
516 lines
17 KiB
Rust
516 lines
17 KiB
Rust
//! Parsers y handlers JSON-RPC de las respuestas/notificaciones LSP.
|
|
|
|
use super::*;
|
|
|
|
pub(crate) fn handle_publish_diagnostics(json: &serde_json::Value, state: &SharedState) {
|
|
let Some(params) = json.get("params") else { return };
|
|
let Some(uri) = params.get("uri").and_then(|u| u.as_str()) else { return };
|
|
let path = match uri.strip_prefix("file://") {
|
|
Some(p) => PathBuf::from(p),
|
|
None => return,
|
|
};
|
|
let Some(diags_arr) = params.get("diagnostics").and_then(|d| d.as_array()) else {
|
|
return;
|
|
};
|
|
let diagnostics: Vec<Diagnostic> = diags_arr
|
|
.iter()
|
|
.filter_map(parse_lsp_diagnostic)
|
|
.collect();
|
|
if let Ok(mut s) = state.lock() {
|
|
s.diagnostics.insert(path, diagnostics);
|
|
}
|
|
}
|
|
|
|
/// Routea una response del server al handler correspondiente según
|
|
/// qué set de pendientes la contenía.
|
|
pub(crate) fn handle_response(id: i64, json: &serde_json::Value, state: &SharedState) {
|
|
let flags = {
|
|
let Ok(mut s) = state.lock() else { return };
|
|
(
|
|
s.pending_completion_ids.remove(&id),
|
|
s.pending_hover_ids.remove(&id),
|
|
s.pending_definition_ids.remove(&id),
|
|
s.pending_formatting_ids.remove(&id),
|
|
s.pending_signature_help_ids.remove(&id),
|
|
s.pending_references_ids.remove(&id),
|
|
s.pending_rename_ids.remove(&id),
|
|
s.pending_document_symbols_ids.remove(&id),
|
|
)
|
|
};
|
|
let (was_completion, was_hover, was_def, was_fmt, was_sig, was_refs, was_rename, was_doc_sym) =
|
|
flags;
|
|
if was_completion {
|
|
handle_completion_response(json, state);
|
|
}
|
|
if was_hover {
|
|
handle_hover_response(json, state);
|
|
}
|
|
if was_def {
|
|
handle_definition_response(json, state);
|
|
}
|
|
if was_fmt {
|
|
handle_text_edits_response(json, state);
|
|
}
|
|
if was_sig {
|
|
handle_signature_help_response(json, state);
|
|
}
|
|
if was_refs {
|
|
handle_references_response(json, state);
|
|
}
|
|
if was_rename {
|
|
handle_rename_response(json, state);
|
|
}
|
|
if was_doc_sym {
|
|
handle_document_symbols_response(json, state);
|
|
}
|
|
}
|
|
|
|
/// Parsea la respuesta de `textDocument/documentSymbol`. Devuelve dos
|
|
/// formatos posibles según la versión del server:
|
|
///
|
|
/// - `DocumentSymbol[]` (jerárquico, moderno) — el que usa rust-analyzer.
|
|
/// - `SymbolInformation[]` (plano, legacy) — fallback razonable.
|
|
///
|
|
/// Ambos se flatten a `Vec<DocumentSymbolEntry>` con depth para que el
|
|
/// caller pueda indentar visualmente.
|
|
pub(crate) fn handle_document_symbols_response(json: &serde_json::Value, state: &SharedState) {
|
|
let Some(result) = json.get("result") else { return };
|
|
if result.is_null() {
|
|
if let Ok(mut s) = state.lock() {
|
|
s.document_symbols.clear();
|
|
}
|
|
return;
|
|
}
|
|
let mut out: Vec<DocumentSymbolEntry> = Vec::new();
|
|
if let Some(arr) = result.as_array() {
|
|
for item in arr {
|
|
// Distingue por la presencia de "selectionRange" (sólo en
|
|
// DocumentSymbol). SymbolInformation tiene "location" en
|
|
// su lugar.
|
|
if item.get("selectionRange").is_some() {
|
|
flatten_document_symbol(item, None, 0, &mut out);
|
|
} else if item.get("location").is_some() {
|
|
if let Some(entry) = parse_symbol_information(item) {
|
|
out.push(entry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Ok(mut s) = state.lock() {
|
|
s.document_symbols = out;
|
|
}
|
|
}
|
|
|
|
/// Flatten recursivo de `DocumentSymbol`. `parent` es el nombre del
|
|
/// contenedor (para que `container` quede poblado en métodos/campos).
|
|
pub(crate) fn flatten_document_symbol(
|
|
node: &serde_json::Value,
|
|
parent: Option<&str>,
|
|
depth: u32,
|
|
out: &mut Vec<DocumentSymbolEntry>,
|
|
) {
|
|
let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("?").to_string();
|
|
let kind_num = node.get("kind").and_then(|v| v.as_u64()).unwrap_or(0);
|
|
let kind = symbol_kind_label(kind_num);
|
|
// `selectionRange.start` es la pos del identificador (lo que el
|
|
// usuario quiere ver al saltar). `range.start` apuntaría al `{` de
|
|
// la definición — menos útil para outline.
|
|
let (line, col) = node
|
|
.get("selectionRange")
|
|
.and_then(|r| r.get("start"))
|
|
.and_then(parse_position)
|
|
.or_else(|| node.get("range").and_then(|r| r.get("start")).and_then(parse_position))
|
|
.unwrap_or((0, 0));
|
|
out.push(DocumentSymbolEntry {
|
|
name: name.clone(),
|
|
kind,
|
|
line,
|
|
col,
|
|
container: parent.map(|s| s.to_string()),
|
|
depth,
|
|
});
|
|
if let Some(children) = node.get("children").and_then(|c| c.as_array()) {
|
|
for child in children {
|
|
flatten_document_symbol(child, Some(&name), depth + 1, out);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_symbol_information(item: &serde_json::Value) -> Option<DocumentSymbolEntry> {
|
|
let name = item.get("name")?.as_str()?.to_string();
|
|
let kind_num = item.get("kind").and_then(|v| v.as_u64()).unwrap_or(0);
|
|
let location = item.get("location")?;
|
|
let (line, col) = location.get("range").and_then(|r| r.get("start")).and_then(parse_position)?;
|
|
let container = item
|
|
.get("containerName")
|
|
.and_then(|v| v.as_str())
|
|
.filter(|s| !s.is_empty())
|
|
.map(|s| s.to_string());
|
|
Some(DocumentSymbolEntry {
|
|
name,
|
|
kind: symbol_kind_label(kind_num),
|
|
line,
|
|
col,
|
|
container,
|
|
depth: 0,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn parse_position(p: &serde_json::Value) -> Option<(usize, usize)> {
|
|
let line = p.get("line")?.as_u64()? as usize;
|
|
let col = p.get("character")?.as_u64()? as usize;
|
|
Some((line, col))
|
|
}
|
|
|
|
/// Mapea el `SymbolKind` numérico del LSP a la etiqueta corta que el
|
|
/// outline pinta. Sólo cubre las que el usuario suele ver — el resto
|
|
/// va a `"sym"`. Lista canónica: <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind>
|
|
pub(crate) fn symbol_kind_label(kind: u64) -> String {
|
|
match kind {
|
|
2 => "mod",
|
|
5 => "class",
|
|
6 => "method",
|
|
7 => "property",
|
|
8 => "field",
|
|
9 => "ctor",
|
|
10 => "enum",
|
|
11 => "iface",
|
|
12 => "fn",
|
|
13 => "var",
|
|
14 => "const",
|
|
15 => "str",
|
|
18 => "arr",
|
|
22 => "variant",
|
|
23 => "struct",
|
|
26 => "type",
|
|
_ => "sym",
|
|
}
|
|
.to_string()
|
|
}
|
|
|
|
pub(crate) fn handle_rename_response(json: &serde_json::Value, state: &SharedState) {
|
|
let Some(result) = json.get("result") else { return };
|
|
if result.is_null() {
|
|
return;
|
|
}
|
|
let mut map: HashMap<PathBuf, Vec<TextEdit>> = HashMap::new();
|
|
// changes: { uri → TextEdit[] }
|
|
if let Some(changes) = result.get("changes").and_then(|c| c.as_object()) {
|
|
for (uri, edits_val) in changes {
|
|
let Some(path) = uri.strip_prefix("file://").map(PathBuf::from) else { continue };
|
|
let Some(arr) = edits_val.as_array() else { continue };
|
|
let edits: Vec<TextEdit> = arr.iter().filter_map(parse_text_edit).collect();
|
|
map.insert(path, edits);
|
|
}
|
|
}
|
|
// documentChanges: [{ textDocument: { uri }, edits: [...] }] — más nuevo.
|
|
if let Some(docs) = result.get("documentChanges").and_then(|c| c.as_array()) {
|
|
for doc in docs {
|
|
let Some(uri) = doc
|
|
.get("textDocument")
|
|
.and_then(|t| t.get("uri"))
|
|
.and_then(|u| u.as_str())
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(path) = uri.strip_prefix("file://").map(PathBuf::from) else { continue };
|
|
let Some(arr) = doc.get("edits").and_then(|e| e.as_array()) else { continue };
|
|
let edits: Vec<TextEdit> = arr.iter().filter_map(parse_text_edit).collect();
|
|
map.entry(path).or_default().extend(edits);
|
|
}
|
|
}
|
|
if let Ok(mut s) = state.lock() {
|
|
s.workspace_edit = map;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_references_response(json: &serde_json::Value, state: &SharedState) {
|
|
let Some(result) = json.get("result") else { return };
|
|
if result.is_null() {
|
|
if let Ok(mut s) = state.lock() {
|
|
s.references.clear();
|
|
}
|
|
return;
|
|
}
|
|
let Some(arr) = result.as_array() else { return };
|
|
let refs: Vec<DefinitionLocation> = arr.iter().filter_map(parse_location).collect();
|
|
if let Ok(mut s) = state.lock() {
|
|
s.references = refs;
|
|
}
|
|
}
|
|
|
|
/// Parsea una `Location` LSP: { uri, range } → DefinitionLocation.
|
|
pub(crate) fn parse_location(loc: &serde_json::Value) -> Option<DefinitionLocation> {
|
|
let uri = loc.get("uri")?.as_str()?;
|
|
let path = uri.strip_prefix("file://").map(PathBuf::from)?;
|
|
let range = loc.get("range")?;
|
|
let start = range.get("start")?;
|
|
let line = start.get("line")?.as_u64()? as usize;
|
|
let col = start.get("character")?.as_u64()? as usize;
|
|
Some(DefinitionLocation { path, line, col })
|
|
}
|
|
|
|
pub(crate) fn handle_signature_help_response(json: &serde_json::Value, state: &SharedState) {
|
|
let Some(result) = json.get("result") else { return };
|
|
if result.is_null() {
|
|
if let Ok(mut s) = state.lock() {
|
|
s.signature_help = None;
|
|
}
|
|
return;
|
|
}
|
|
let info = parse_signature_help(result);
|
|
if let Ok(mut s) = state.lock() {
|
|
s.signature_help = info;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_signature_help(result: &serde_json::Value) -> Option<SignatureHelpInfo> {
|
|
let sigs = result.get("signatures")?.as_array()?;
|
|
if sigs.is_empty() {
|
|
return None;
|
|
}
|
|
let active_sig = result.get("activeSignature").and_then(|n| n.as_u64()).unwrap_or(0) as usize;
|
|
let sig = sigs.get(active_sig).or_else(|| sigs.first())?;
|
|
let label = sig.get("label")?.as_str()?.to_string();
|
|
let doc = sig
|
|
.get("documentation")
|
|
.map(stringify_hover_contents)
|
|
.filter(|s| !s.is_empty());
|
|
let active_param = sig
|
|
.get("activeParameter")
|
|
.or_else(|| result.get("activeParameter"))
|
|
.and_then(|n| n.as_u64())
|
|
.unwrap_or(0) as usize;
|
|
let param_labels = sig
|
|
.get("parameters")
|
|
.and_then(|p| p.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|p| {
|
|
let lbl = p.get("label")?;
|
|
if let Some(s) = lbl.as_str() {
|
|
Some(s.to_string())
|
|
} else if let Some(arr2) = lbl.as_array() {
|
|
let s = arr2.first()?.as_u64()? as usize;
|
|
let e = arr2.get(1)?.as_u64()? as usize;
|
|
label.get(s..e).map(String::from)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
Some(SignatureHelpInfo { label, doc, active_param, param_labels })
|
|
}
|
|
|
|
pub(crate) fn handle_text_edits_response(json: &serde_json::Value, state: &SharedState) {
|
|
let Some(result) = json.get("result") else { return };
|
|
if result.is_null() {
|
|
return;
|
|
}
|
|
let Some(arr) = result.as_array() else { return };
|
|
let edits: Vec<TextEdit> = arr.iter().filter_map(parse_text_edit).collect();
|
|
if let Ok(mut s) = state.lock() {
|
|
s.text_edits = edits;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_text_edit(v: &serde_json::Value) -> Option<TextEdit> {
|
|
let range = v.get("range")?;
|
|
let start = range.get("start")?;
|
|
let end = range.get("end")?;
|
|
let start_line = start.get("line")?.as_u64()? as usize;
|
|
let start_col = start.get("character")?.as_u64()? as usize;
|
|
let end_line = end.get("line")?.as_u64()? as usize;
|
|
let end_col = end.get("character")?.as_u64()? as usize;
|
|
let new_text = v.get("newText")?.as_str()?.to_string();
|
|
Some(TextEdit { start_line, start_col, end_line, end_col, new_text })
|
|
}
|
|
|
|
pub(crate) fn handle_definition_response(json: &serde_json::Value, state: &SharedState) {
|
|
let Some(result) = json.get("result") else { return };
|
|
if result.is_null() {
|
|
return;
|
|
}
|
|
// `result` puede ser:
|
|
// - Location { uri, range }
|
|
// - Location[]
|
|
// - LocationLink[] { targetUri, targetSelectionRange }
|
|
// Tomamos la primera location en cualquier caso.
|
|
let loc_value = if result.is_array() {
|
|
result.as_array().and_then(|a| a.first()).cloned()
|
|
} else {
|
|
Some(result.clone())
|
|
};
|
|
let Some(loc) = loc_value else { return };
|
|
|
|
let (uri, range) = if let Some(u) = loc.get("uri") {
|
|
(u, loc.get("range"))
|
|
} else if let Some(u) = loc.get("targetUri") {
|
|
(
|
|
u,
|
|
loc.get("targetSelectionRange").or_else(|| loc.get("targetRange")),
|
|
)
|
|
} else {
|
|
return;
|
|
};
|
|
let Some(uri) = uri.as_str() else { return };
|
|
let path = match uri.strip_prefix("file://") {
|
|
Some(p) => PathBuf::from(p),
|
|
None => return,
|
|
};
|
|
let Some(range) = range else { return };
|
|
let Some(start) = range.get("start") else { return };
|
|
let line = start.get("line").and_then(|n| n.as_u64()).unwrap_or(0) as usize;
|
|
let col = start.get("character").and_then(|n| n.as_u64()).unwrap_or(0) as usize;
|
|
if let Ok(mut s) = state.lock() {
|
|
s.definition = Some(DefinitionLocation { path, line, col });
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_completion_response(json: &serde_json::Value, state: &SharedState) {
|
|
let Some(result) = json.get("result") else { return };
|
|
let items_arr = if let Some(arr) = result.as_array() {
|
|
arr.clone()
|
|
} else if let Some(items) = result.get("items").and_then(|i| i.as_array()) {
|
|
items.clone()
|
|
} else {
|
|
return;
|
|
};
|
|
let completions: Vec<CompletionItem> = items_arr.iter().filter_map(parse_completion).collect();
|
|
if let Ok(mut s) = state.lock() {
|
|
s.completions = completions;
|
|
}
|
|
}
|
|
|
|
pub(crate) fn handle_hover_response(json: &serde_json::Value, state: &SharedState) {
|
|
let Some(result) = json.get("result") else { return };
|
|
if result.is_null() {
|
|
if let Ok(mut s) = state.lock() {
|
|
s.hover = None;
|
|
}
|
|
return;
|
|
}
|
|
let info = parse_hover(result);
|
|
if let Ok(mut s) = state.lock() {
|
|
s.hover = info;
|
|
}
|
|
}
|
|
|
|
/// `contents` en LSP puede ser:
|
|
/// - String
|
|
/// - { kind: "markdown"|"plaintext", value: String }
|
|
/// - Array de los anteriores (deprecated pero algunos servers lo mandan)
|
|
/// - { language: ..., value: ... } (legacy MarkedString)
|
|
pub(crate) fn parse_hover(result: &serde_json::Value) -> Option<HoverInfo> {
|
|
let contents = result.get("contents")?;
|
|
let text = stringify_hover_contents(contents);
|
|
if text.is_empty() {
|
|
None
|
|
} else {
|
|
Some(HoverInfo { contents: text })
|
|
}
|
|
}
|
|
|
|
pub(crate) fn stringify_hover_contents(v: &serde_json::Value) -> String {
|
|
match v {
|
|
serde_json::Value::String(s) => s.clone(),
|
|
serde_json::Value::Object(map) => {
|
|
// { kind, value } o { language, value }
|
|
map.get("value")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string()
|
|
}
|
|
serde_json::Value::Array(arr) => arr
|
|
.iter()
|
|
.map(stringify_hover_contents)
|
|
.filter(|s| !s.is_empty())
|
|
.collect::<Vec<_>>()
|
|
.join("\n"),
|
|
_ => String::new(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_completion(v: &serde_json::Value) -> Option<CompletionItem> {
|
|
let label = v.get("label")?.as_str()?.to_string();
|
|
let insert_text = v
|
|
.get("insertText")
|
|
.and_then(|s| s.as_str())
|
|
.map(String::from);
|
|
let kind = v
|
|
.get("kind")
|
|
.and_then(|k| k.as_u64())
|
|
.map(|n| completion_kind_label(n).to_string());
|
|
let detail = v
|
|
.get("detail")
|
|
.and_then(|d| d.as_str())
|
|
.map(String::from);
|
|
Some(CompletionItem { label, insert_text, kind, detail })
|
|
}
|
|
|
|
/// Etiqueta corta para el CompletionItemKind de LSP (1..25).
|
|
pub(crate) fn completion_kind_label(k: u64) -> &'static str {
|
|
match k {
|
|
1 => "Text",
|
|
2 => "Method",
|
|
3 => "Function",
|
|
4 => "Ctor",
|
|
5 => "Field",
|
|
6 => "Var",
|
|
7 => "Class",
|
|
8 => "Iface",
|
|
9 => "Mod",
|
|
10 => "Prop",
|
|
11 => "Unit",
|
|
12 => "Value",
|
|
13 => "Enum",
|
|
14 => "Keyword",
|
|
15 => "Snip",
|
|
16 => "Color",
|
|
17 => "File",
|
|
18 => "Ref",
|
|
19 => "Folder",
|
|
20 => "EnumMember",
|
|
21 => "Const",
|
|
22 => "Struct",
|
|
23 => "Event",
|
|
24 => "Op",
|
|
25 => "TypeParam",
|
|
_ => "?",
|
|
}
|
|
}
|
|
|
|
pub(crate) fn parse_lsp_diagnostic(d: &serde_json::Value) -> Option<Diagnostic> {
|
|
let range = d.get("range")?;
|
|
let start = range.get("start")?;
|
|
let end = range.get("end")?;
|
|
let sl = start.get("line")?.as_u64()? as usize;
|
|
let sc = start.get("character")?.as_u64()? as usize;
|
|
let el = end.get("line")?.as_u64()? as usize;
|
|
let ec = end.get("character")?.as_u64()? as usize;
|
|
let severity = match d.get("severity").and_then(|s| s.as_u64()) {
|
|
Some(1) => Severity::Error,
|
|
Some(2) => Severity::Warning,
|
|
Some(3) => Severity::Information,
|
|
Some(4) => Severity::Hint,
|
|
_ => Severity::Information,
|
|
};
|
|
let message = d
|
|
.get("message")
|
|
.and_then(|m| m.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let source = d.get("source").and_then(|s| s.as_str()).map(String::from);
|
|
Some(Diagnostic {
|
|
range: DiagnosticRange {
|
|
start: Pos::new(sl, sc),
|
|
end: Pos::new(el, ec),
|
|
},
|
|
severity,
|
|
message,
|
|
source,
|
|
})
|
|
}
|