gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ 14d76bd4af13b219f0b162a54f09e784dea9aa8d widgets/text-editor-lsp/src/lib.rs Rust · 275 lines · 11.4 KB History Who touched what raw

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

1//! `llimphi-widget-text-editor-lsp` — cliente LSP para alimentar
2//! diagnostics al editor.
3//!
4//! Implementación real basada en `tokio::process::Command` +
5//! `lsp-types` + JSON-RPC sobre stdin/stdout del language server.
6//!
7//! Flujo:
8//!
9//! 1. `RustAnalyzerClient::start(workspace_root)` spawn `rust-analyzer`
10//!    (o el binary que se le pase con `with_command`) y arranca dos
11//!    tasks tokio:
12//!    - **writer**: consume mensajes del `mpsc::Sender`, los serializa
13//!      con headers `Content-Length: N\r\n\r\n` y los manda al stdin.
14//!    - **reader**: parsea el stdout del server (mismo formato),
15//!      atiende `textDocument/publishDiagnostics` y guarda los
16//!      diagnostics en el state compartido.
17//! 2. El handshake `initialize` se envía sincronicamente desde `start`
18//!    y se espera la respuesta antes de mandar `initialized` +
19//!    procesar más mensajes.
20//! 3. `did_open` / `did_change` / `did_close` mandan las notifications
21//!    correspondientes — sin esperar respuesta.
22//! 4. `diagnostics(path)` lee del state sin contactar al server.
23//!
24//! El client maneja **una sola conexión por instancia**. Para
25//! multi-proyecto el caller crea varios clients.
26//!
27//! Si el server no se puede spawnear (binary no instalado), el client
28//! cae en modo no-op transparentemente — `diagnostics` devuelve vacío.
29
30#![forbid(unsafe_code)]
31
32use std::collections::HashMap;
33use std::path::{Path, PathBuf};
34use std::sync::{Arc, Mutex};
35
36use llimphi_widget_text_editor::{Diagnostic, DiagnosticRange, Pos, Severity};
37
38/// Item de completion — mirror minimal de `lsp_types::CompletionItem`.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct CompletionItem {
41    pub label: String,
42    /// Texto a insertar. Si `None`, se usa `label`.
43    pub insert_text: Option<String>,
44    /// Tipo del símbolo según LSP (Function, Variable, etc.) — para
45    /// mostrar un ícono. Aquí lo guardamos como string corto.
46    pub kind: Option<String>,
47    /// Documentación corta — el primer renglón típicamente.
48    pub detail: Option<String>,
49}
50
51impl CompletionItem {
52    pub fn text_to_insert(&self) -> &str {
53        self.insert_text.as_deref().unwrap_or(self.label.as_str())
54    }
55}
56
57/// Contrato que un client LSP debe cumplir para alimentar al editor.
58pub trait LspClient: Send {
59    fn diagnostics(&self, path: &Path) -> Vec<Diagnostic>;
60    fn did_open(&mut self, path: &Path, language: &str, text: &str);
61    fn did_change(&mut self, path: &Path, new_text: &str);
62    fn did_close(&mut self, path: &Path);
63    /// Dispara una petición de completions en `(line, col)` del path.
64    /// Fire-and-forget; la respuesta se lee con `latest_completions`.
65    fn request_completions(&mut self, path: &Path, line: usize, col: usize);
66    /// Última lista de completions recibida (cualquier path/pos).
67    /// Vacío hasta que el server responda. El client la limpia cuando
68    /// el caller llama `clear_completions`.
69    fn latest_completions(&self) -> Vec<CompletionItem>;
70    /// Borra el cache de completions — útil al cerrar el popup.
71    fn clear_completions(&mut self);
72    /// Dispara textDocument/hover. Fire-and-forget; el caller polla
73    /// `latest_hover` para leer la respuesta.
74    fn request_hover(&mut self, path: &Path, line: usize, col: usize);
75    /// Última hover info recibida (cualquier path/pos).
76    fn latest_hover(&self) -> Option<HoverInfo>;
77    /// Borra el cache de hover.
78    fn clear_hover(&mut self);
79    /// Dispara textDocument/definition. Fire-and-forget; el caller
80    /// polla `latest_definition`.
81    fn request_definition(&mut self, path: &Path, line: usize, col: usize);
82    /// Última definition recibida (path destino + pos de inicio).
83    fn latest_definition(&self) -> Option<DefinitionLocation>;
84    fn clear_definition(&mut self);
85    /// Dispara textDocument/formatting. Cuando llega la response, el
86    /// caller polla `latest_text_edits` y los aplica al buffer.
87    fn request_formatting(&mut self, path: &Path, tab_size: u32, insert_spaces: bool);
88    /// Última lista de TextEdits recibida (de formatting o rename).
89    fn latest_text_edits(&self) -> Vec<TextEdit>;
90    fn clear_text_edits(&mut self);
91    /// Dispara textDocument/signatureHelp. Cuando llega, el popup
92    /// muestra la firma activa con el parámetro current resaltado.
93    fn request_signature_help(&mut self, path: &Path, line: usize, col: usize);
94    fn latest_signature_help(&self) -> Option<SignatureHelpInfo>;
95    fn clear_signature_help(&mut self);
96    /// Dispara textDocument/references. `include_decl` controla si la
97    /// declaración misma aparece en los resultados.
98    fn request_references(&mut self, path: &Path, line: usize, col: usize, include_decl: bool);
99    fn latest_references(&self) -> Vec<DefinitionLocation>;
100    fn clear_references(&mut self);
101    /// Dispara textDocument/rename con `new_name` como nuevo identificador.
102    fn request_rename(&mut self, path: &Path, line: usize, col: usize, new_name: &str);
103    /// Última WorkspaceEdit recibida (rename o code actions). Mapeado a
104    /// `path → Vec<TextEdit>` por simplicidad.
105    fn latest_workspace_edit(&self) -> std::collections::HashMap<PathBuf, Vec<TextEdit>>;
106    fn clear_workspace_edit(&mut self);
107
108    /// Dispara textDocument/documentSymbol. La respuesta llega
109    /// asincrónica; el caller la recoge con [`latest_document_symbols`].
110    fn request_document_symbols(&mut self, path: &Path);
111    /// Última respuesta de documentSymbol — lista plana flattening del
112    /// árbol jerárquico que devuelve el server. Orden: top-down,
113    /// children en orden de aparición. `depth` refleja la profundidad
114    /// para que el caller indente visualmente.
115    fn latest_document_symbols(&self) -> Vec<DocumentSymbolEntry>;
116    fn clear_document_symbols(&mut self);
117}
118
119/// Una entrada flattening del árbol `DocumentSymbol` del LSP. Espejo
120/// mínimo que evita arrastrar `lsp_types::SymbolKind` a los hosts —
121/// `kind` viene ya como string corta (`"fn"`, `"struct"`, `"method"`, …).
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct DocumentSymbolEntry {
124    pub name: String,
125    pub kind: String,
126    pub line: usize,
127    pub col: usize,
128    pub container: Option<String>,
129    pub depth: u32,
130}
131
132/// Info de signatureHelp activa.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct SignatureHelpInfo {
135    /// Firma activa (label completa, ej. "fn foo(x: i32, y: String) -> u64").
136    pub label: String,
137    /// Documentación de la firma activa.
138    pub doc: Option<String>,
139    /// Índice del parámetro current (0-based).
140    pub active_param: usize,
141    /// Labels de los parámetros — para resaltar el activo.
142    pub param_labels: Vec<String>,
143}
144
145/// Edit estilo LSP: reemplazar el rango `[start..end)` por `new_text`.
146/// Para apply: ordenar desc por `start` y aplicar uno por uno.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct TextEdit {
149    pub start_line: usize,
150    pub start_col: usize,
151    pub end_line: usize,
152    pub end_col: usize,
153    pub new_text: String,
154}
155
156/// Resultado de un goto-definition: archivo destino + posición.
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct DefinitionLocation {
159    pub path: PathBuf,
160    pub line: usize,
161    pub col: usize,
162}
163
164/// Información de hover — espejo simplificado de `lsp_types::Hover`.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct HoverInfo {
167    /// Markdown / plaintext del símbolo bajo el cursor. El render del
168    /// caller lo muestra tal cual (sin parsear markdown todavía).
169    pub contents: String,
170}
171
172/// Stub que no hace nada — útil cuando no hay LSP configurado o para tests.
173#[derive(Debug, Default, Clone, Copy)]
174pub struct NoopLspClient;
175
176impl LspClient for NoopLspClient {
177    fn diagnostics(&self, _: &Path) -> Vec<Diagnostic> {
178        Vec::new()
179    }
180    fn did_open(&mut self, _: &Path, _: &str, _: &str) {}
181    fn did_change(&mut self, _: &Path, _: &str) {}
182    fn did_close(&mut self, _: &Path) {}
183    fn request_completions(&mut self, _: &Path, _: usize, _: usize) {}
184    fn latest_completions(&self) -> Vec<CompletionItem> {
185        Vec::new()
186    }
187    fn clear_completions(&mut self) {}
188    fn request_hover(&mut self, _: &Path, _: usize, _: usize) {}
189    fn latest_hover(&self) -> Option<HoverInfo> {
190        None
191    }
192    fn clear_hover(&mut self) {}
193    fn request_definition(&mut self, _: &Path, _: usize, _: usize) {}
194    fn latest_definition(&self) -> Option<DefinitionLocation> {
195        None
196    }
197    fn clear_definition(&mut self) {}
198    fn request_formatting(&mut self, _: &Path, _: u32, _: bool) {}
199    fn latest_text_edits(&self) -> Vec<TextEdit> {
200        Vec::new()
201    }
202    fn clear_text_edits(&mut self) {}
203    fn request_signature_help(&mut self, _: &Path, _: usize, _: usize) {}
204    fn latest_signature_help(&self) -> Option<SignatureHelpInfo> {
205        None
206    }
207    fn clear_signature_help(&mut self) {}
208    fn request_references(&mut self, _: &Path, _: usize, _: usize, _: bool) {}
209    fn latest_references(&self) -> Vec<DefinitionLocation> {
210        Vec::new()
211    }
212    fn clear_references(&mut self) {}
213    fn request_rename(&mut self, _: &Path, _: usize, _: usize, _: &str) {}
214    fn latest_workspace_edit(&self) -> std::collections::HashMap<PathBuf, Vec<TextEdit>> {
215        std::collections::HashMap::new()
216    }
217    fn clear_workspace_edit(&mut self) {}
218    fn request_document_symbols(&mut self, _: &Path) {}
219    fn latest_document_symbols(&self) -> Vec<DocumentSymbolEntry> {
220        Vec::new()
221    }
222    fn clear_document_symbols(&mut self) {}
223}
224
225// ---------------------------------------------------------------------
226// Rust-analyzer client real
227// ---------------------------------------------------------------------
228
229/// State compartido: paths → versión + diagnostics actuales + última
230/// lista de completions recibida.
231#[derive(Default)]
232struct SharedInner {
233    diagnostics: HashMap<PathBuf, Vec<Diagnostic>>,
234    /// Última respuesta de completions — sobreescribe cualquier
235    /// request previo. El caller decide cuándo limpiar.
236    completions: Vec<CompletionItem>,
237    /// Última hover info recibida.
238    hover: Option<HoverInfo>,
239    /// Última definition recibida.
240    definition: Option<DefinitionLocation>,
241    /// Última lista de TextEdits (formatting / rename).
242    text_edits: Vec<TextEdit>,
243    /// Última signature help.
244    signature_help: Option<SignatureHelpInfo>,
245    /// Última lista de references.
246    references: Vec<DefinitionLocation>,
247    /// Última WorkspaceEdit (de rename). Mapeo path → edits.
248    workspace_edit: HashMap<PathBuf, Vec<TextEdit>>,
249    /// Última lista de document symbols (flattened del árbol que devuelve
250    /// el server). Se sobreescribe en cada request.
251    document_symbols: Vec<DocumentSymbolEntry>,
252    /// IDs de requests pendientes para distinguir responses; el reader
253    /// usa estos sets para routear cada response al handler correcto.
254    pending_completion_ids: std::collections::HashSet<i64>,
255    pending_hover_ids: std::collections::HashSet<i64>,
256    pending_definition_ids: std::collections::HashSet<i64>,
257    pending_formatting_ids: std::collections::HashSet<i64>,
258    pending_signature_help_ids: std::collections::HashSet<i64>,
259    pending_references_ids: std::collections::HashSet<i64>,
260    pending_rename_ids: std::collections::HashSet<i64>,
261    pending_document_symbols_ids: std::collections::HashSet<i64>,
262}
263
264type SharedState = Arc<Mutex<SharedInner>>;
265
266// Cliente y protocolo partidos del monolito (regla dura #1, 1660 LOC):
267// `client` (RustAnalyzerClient + impls), `protocol` (parsers/handlers JSON-RPC).
268mod client;
269mod protocol;
270
271pub use client::RustAnalyzerClient;
272pub(crate) use protocol::*;
273
274#[cfg(test)]
275mod tests;