tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 14d76bd4af13b219f0b162a54f09e784dea9aa8d
widgets/text-editor-lsp/src/lib.rs
History
Who touched what
raw
feat: llimphi standalone — framework UI soberano extraído del monorepo
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;