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

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>
This commit is contained in:
2026-06-04 04:23:42 +00:00
commit e65e9cc623
286 changed files with 46136 additions and 0 deletions
+275
View File
@@ -0,0 +1,275 @@
//! `llimphi-widget-text-editor-lsp` — cliente LSP para alimentar
//! diagnostics al editor.
//!
//! Implementación real basada en `tokio::process::Command` +
//! `lsp-types` + JSON-RPC sobre stdin/stdout del language server.
//!
//! Flujo:
//!
//! 1. `RustAnalyzerClient::start(workspace_root)` spawn `rust-analyzer`
//! (o el binary que se le pase con `with_command`) y arranca dos
//! tasks tokio:
//! - **writer**: consume mensajes del `mpsc::Sender`, los serializa
//! con headers `Content-Length: N\r\n\r\n` y los manda al stdin.
//! - **reader**: parsea el stdout del server (mismo formato),
//! atiende `textDocument/publishDiagnostics` y guarda los
//! diagnostics en el state compartido.
//! 2. El handshake `initialize` se envía sincronicamente desde `start`
//! y se espera la respuesta antes de mandar `initialized` +
//! procesar más mensajes.
//! 3. `did_open` / `did_change` / `did_close` mandan las notifications
//! correspondientes — sin esperar respuesta.
//! 4. `diagnostics(path)` lee del state sin contactar al server.
//!
//! El client maneja **una sola conexión por instancia**. Para
//! multi-proyecto el caller crea varios clients.
//!
//! Si el server no se puede spawnear (binary no instalado), el client
//! cae en modo no-op transparentemente — `diagnostics` devuelve vacío.
#![forbid(unsafe_code)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use llimphi_widget_text_editor::{Diagnostic, DiagnosticRange, Pos, Severity};
/// Item de completion — mirror minimal de `lsp_types::CompletionItem`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompletionItem {
pub label: String,
/// Texto a insertar. Si `None`, se usa `label`.
pub insert_text: Option<String>,
/// Tipo del símbolo según LSP (Function, Variable, etc.) — para
/// mostrar un ícono. Aquí lo guardamos como string corto.
pub kind: Option<String>,
/// Documentación corta — el primer renglón típicamente.
pub detail: Option<String>,
}
impl CompletionItem {
pub fn text_to_insert(&self) -> &str {
self.insert_text.as_deref().unwrap_or(self.label.as_str())
}
}
/// Contrato que un client LSP debe cumplir para alimentar al editor.
pub trait LspClient: Send {
fn diagnostics(&self, path: &Path) -> Vec<Diagnostic>;
fn did_open(&mut self, path: &Path, language: &str, text: &str);
fn did_change(&mut self, path: &Path, new_text: &str);
fn did_close(&mut self, path: &Path);
/// Dispara una petición de completions en `(line, col)` del path.
/// Fire-and-forget; la respuesta se lee con `latest_completions`.
fn request_completions(&mut self, path: &Path, line: usize, col: usize);
/// Última lista de completions recibida (cualquier path/pos).
/// Vacío hasta que el server responda. El client la limpia cuando
/// el caller llama `clear_completions`.
fn latest_completions(&self) -> Vec<CompletionItem>;
/// Borra el cache de completions — útil al cerrar el popup.
fn clear_completions(&mut self);
/// Dispara textDocument/hover. Fire-and-forget; el caller polla
/// `latest_hover` para leer la respuesta.
fn request_hover(&mut self, path: &Path, line: usize, col: usize);
/// Última hover info recibida (cualquier path/pos).
fn latest_hover(&self) -> Option<HoverInfo>;
/// Borra el cache de hover.
fn clear_hover(&mut self);
/// Dispara textDocument/definition. Fire-and-forget; el caller
/// polla `latest_definition`.
fn request_definition(&mut self, path: &Path, line: usize, col: usize);
/// Última definition recibida (path destino + pos de inicio).
fn latest_definition(&self) -> Option<DefinitionLocation>;
fn clear_definition(&mut self);
/// Dispara textDocument/formatting. Cuando llega la response, el
/// caller polla `latest_text_edits` y los aplica al buffer.
fn request_formatting(&mut self, path: &Path, tab_size: u32, insert_spaces: bool);
/// Última lista de TextEdits recibida (de formatting o rename).
fn latest_text_edits(&self) -> Vec<TextEdit>;
fn clear_text_edits(&mut self);
/// Dispara textDocument/signatureHelp. Cuando llega, el popup
/// muestra la firma activa con el parámetro current resaltado.
fn request_signature_help(&mut self, path: &Path, line: usize, col: usize);
fn latest_signature_help(&self) -> Option<SignatureHelpInfo>;
fn clear_signature_help(&mut self);
/// Dispara textDocument/references. `include_decl` controla si la
/// declaración misma aparece en los resultados.
fn request_references(&mut self, path: &Path, line: usize, col: usize, include_decl: bool);
fn latest_references(&self) -> Vec<DefinitionLocation>;
fn clear_references(&mut self);
/// Dispara textDocument/rename con `new_name` como nuevo identificador.
fn request_rename(&mut self, path: &Path, line: usize, col: usize, new_name: &str);
/// Última WorkspaceEdit recibida (rename o code actions). Mapeado a
/// `path → Vec<TextEdit>` por simplicidad.
fn latest_workspace_edit(&self) -> std::collections::HashMap<PathBuf, Vec<TextEdit>>;
fn clear_workspace_edit(&mut self);
/// Dispara textDocument/documentSymbol. La respuesta llega
/// asincrónica; el caller la recoge con [`latest_document_symbols`].
fn request_document_symbols(&mut self, path: &Path);
/// Última respuesta de documentSymbol — lista plana flattening del
/// árbol jerárquico que devuelve el server. Orden: top-down,
/// children en orden de aparición. `depth` refleja la profundidad
/// para que el caller indente visualmente.
fn latest_document_symbols(&self) -> Vec<DocumentSymbolEntry>;
fn clear_document_symbols(&mut self);
}
/// Una entrada flattening del árbol `DocumentSymbol` del LSP. Espejo
/// mínimo que evita arrastrar `lsp_types::SymbolKind` a los hosts —
/// `kind` viene ya como string corta (`"fn"`, `"struct"`, `"method"`, …).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentSymbolEntry {
pub name: String,
pub kind: String,
pub line: usize,
pub col: usize,
pub container: Option<String>,
pub depth: u32,
}
/// Info de signatureHelp activa.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignatureHelpInfo {
/// Firma activa (label completa, ej. "fn foo(x: i32, y: String) -> u64").
pub label: String,
/// Documentación de la firma activa.
pub doc: Option<String>,
/// Índice del parámetro current (0-based).
pub active_param: usize,
/// Labels de los parámetros — para resaltar el activo.
pub param_labels: Vec<String>,
}
/// Edit estilo LSP: reemplazar el rango `[start..end)` por `new_text`.
/// Para apply: ordenar desc por `start` y aplicar uno por uno.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextEdit {
pub start_line: usize,
pub start_col: usize,
pub end_line: usize,
pub end_col: usize,
pub new_text: String,
}
/// Resultado de un goto-definition: archivo destino + posición.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DefinitionLocation {
pub path: PathBuf,
pub line: usize,
pub col: usize,
}
/// Información de hover — espejo simplificado de `lsp_types::Hover`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HoverInfo {
/// Markdown / plaintext del símbolo bajo el cursor. El render del
/// caller lo muestra tal cual (sin parsear markdown todavía).
pub contents: String,
}
/// Stub que no hace nada — útil cuando no hay LSP configurado o para tests.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopLspClient;
impl LspClient for NoopLspClient {
fn diagnostics(&self, _: &Path) -> Vec<Diagnostic> {
Vec::new()
}
fn did_open(&mut self, _: &Path, _: &str, _: &str) {}
fn did_change(&mut self, _: &Path, _: &str) {}
fn did_close(&mut self, _: &Path) {}
fn request_completions(&mut self, _: &Path, _: usize, _: usize) {}
fn latest_completions(&self) -> Vec<CompletionItem> {
Vec::new()
}
fn clear_completions(&mut self) {}
fn request_hover(&mut self, _: &Path, _: usize, _: usize) {}
fn latest_hover(&self) -> Option<HoverInfo> {
None
}
fn clear_hover(&mut self) {}
fn request_definition(&mut self, _: &Path, _: usize, _: usize) {}
fn latest_definition(&self) -> Option<DefinitionLocation> {
None
}
fn clear_definition(&mut self) {}
fn request_formatting(&mut self, _: &Path, _: u32, _: bool) {}
fn latest_text_edits(&self) -> Vec<TextEdit> {
Vec::new()
}
fn clear_text_edits(&mut self) {}
fn request_signature_help(&mut self, _: &Path, _: usize, _: usize) {}
fn latest_signature_help(&self) -> Option<SignatureHelpInfo> {
None
}
fn clear_signature_help(&mut self) {}
fn request_references(&mut self, _: &Path, _: usize, _: usize, _: bool) {}
fn latest_references(&self) -> Vec<DefinitionLocation> {
Vec::new()
}
fn clear_references(&mut self) {}
fn request_rename(&mut self, _: &Path, _: usize, _: usize, _: &str) {}
fn latest_workspace_edit(&self) -> std::collections::HashMap<PathBuf, Vec<TextEdit>> {
std::collections::HashMap::new()
}
fn clear_workspace_edit(&mut self) {}
fn request_document_symbols(&mut self, _: &Path) {}
fn latest_document_symbols(&self) -> Vec<DocumentSymbolEntry> {
Vec::new()
}
fn clear_document_symbols(&mut self) {}
}
// ---------------------------------------------------------------------
// Rust-analyzer client real
// ---------------------------------------------------------------------
/// State compartido: paths → versión + diagnostics actuales + última
/// lista de completions recibida.
#[derive(Default)]
struct SharedInner {
diagnostics: HashMap<PathBuf, Vec<Diagnostic>>,
/// Última respuesta de completions — sobreescribe cualquier
/// request previo. El caller decide cuándo limpiar.
completions: Vec<CompletionItem>,
/// Última hover info recibida.
hover: Option<HoverInfo>,
/// Última definition recibida.
definition: Option<DefinitionLocation>,
/// Última lista de TextEdits (formatting / rename).
text_edits: Vec<TextEdit>,
/// Última signature help.
signature_help: Option<SignatureHelpInfo>,
/// Última lista de references.
references: Vec<DefinitionLocation>,
/// Última WorkspaceEdit (de rename). Mapeo path → edits.
workspace_edit: HashMap<PathBuf, Vec<TextEdit>>,
/// Última lista de document symbols (flattened del árbol que devuelve
/// el server). Se sobreescribe en cada request.
document_symbols: Vec<DocumentSymbolEntry>,
/// IDs de requests pendientes para distinguir responses; el reader
/// usa estos sets para routear cada response al handler correcto.
pending_completion_ids: std::collections::HashSet<i64>,
pending_hover_ids: std::collections::HashSet<i64>,
pending_definition_ids: std::collections::HashSet<i64>,
pending_formatting_ids: std::collections::HashSet<i64>,
pending_signature_help_ids: std::collections::HashSet<i64>,
pending_references_ids: std::collections::HashSet<i64>,
pending_rename_ids: std::collections::HashSet<i64>,
pending_document_symbols_ids: std::collections::HashSet<i64>,
}
type SharedState = Arc<Mutex<SharedInner>>;
// Cliente y protocolo partidos del monolito (regla dura #1, 1660 LOC):
// `client` (RustAnalyzerClient + impls), `protocol` (parsers/handlers JSON-RPC).
mod client;
mod protocol;
pub use client::RustAnalyzerClient;
pub(crate) use protocol::*;
#[cfg(test)]
mod tests;