tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 14d76bd4af13b219f0b162a54f09e784dea9aa8d
widgets/text-editor-lsp/src/tests.rs
History
Who touched what
raw
feat: llimphi standalone — framework UI soberano extraído del monorepo
1use super::*;
2
3#[test]
4fn noop_devuelve_vacio() {
5 let c = NoopLspClient;
6 assert!(c.diagnostics(&PathBuf::from("x")).is_empty());
7}
8
9#[test]
10fn noop_no_panic_en_eventos() {
11 let mut c = NoopLspClient;
12 c.did_open(&PathBuf::from("x"), "rust", "fn main() {}");
13 c.did_change(&PathBuf::from("x"), "fn main() { 1 }");
14 c.did_close(&PathBuf::from("x"));
15}
16
17#[test]
18fn parse_diagnostic_minimo() {
19 let json = serde_json::json!({
20 "range": {
21 "start": { "line": 3, "character": 5 },
22 "end": { "line": 3, "character": 12 }
23 },
24 "severity": 1,
25 "message": "no es así",
26 "source": "rustc"
27 });
28 let d = parse_lsp_diagnostic(&json).unwrap();
29 assert_eq!(d.range.start, Pos::new(3, 5));
30 assert_eq!(d.range.end, Pos::new(3, 12));
31 assert_eq!(d.severity, Severity::Error);
32 assert_eq!(d.message, "no es así");
33 assert_eq!(d.source.as_deref(), Some("rustc"));
34}
35
36#[test]
37fn parse_diagnostic_sin_severidad_es_info() {
38 let json = serde_json::json!({
39 "range": {
40 "start": { "line": 0, "character": 0 },
41 "end": { "line": 0, "character": 1 }
42 },
43 "message": "x"
44 });
45 let d = parse_lsp_diagnostic(&json).unwrap();
46 assert_eq!(d.severity, Severity::Information);
47}
48
49#[test]
50fn parse_completion_minimo() {
51 let v = serde_json::json!({
52 "label": "to_string",
53 "insertText": "to_string()",
54 "kind": 2,
55 "detail": "fn(&self) -> String"
56 });
57 let c = parse_completion(&v).unwrap();
58 assert_eq!(c.label, "to_string");
59 assert_eq!(c.insert_text.as_deref(), Some("to_string()"));
60 assert_eq!(c.kind.as_deref(), Some("Method"));
61 assert_eq!(c.detail.as_deref(), Some("fn(&self) -> String"));
62}
63
64#[test]
65fn parse_hover_string_simple() {
66 let v = serde_json::json!({ "contents": "hola" });
67 let h = parse_hover(&v).unwrap();
68 assert_eq!(h.contents, "hola");
69}
70
71#[test]
72fn parse_hover_marked_object() {
73 let v = serde_json::json!({
74 "contents": { "kind": "markdown", "value": "**fn**(x: i32) -> i32" }
75 });
76 let h = parse_hover(&v).unwrap();
77 assert_eq!(h.contents, "**fn**(x: i32) -> i32");
78}
79
80#[test]
81fn parse_hover_array_concatena() {
82 let v = serde_json::json!({
83 "contents": ["primero", { "value": "segundo" }, ""]
84 });
85 let h = parse_hover(&v).unwrap();
86 assert_eq!(h.contents, "primero\nsegundo");
87}
88
89#[test]
90fn parse_hover_vacio_devuelve_none() {
91 let v = serde_json::json!({ "contents": "" });
92 assert!(parse_hover(&v).is_none());
93}
94
95#[test]
96fn parse_completion_sin_insert_text_usa_label() {
97 let v = serde_json::json!({ "label": "main" });
98 let c = parse_completion(&v).unwrap();
99 assert_eq!(c.text_to_insert(), "main");
100}
101
102fn make_state() -> SharedState {
103 Arc::new(Mutex::new(SharedInner::default()))
104}
105
106#[test]
107fn handle_rename_changes_map() {
108 let s = make_state();
109 let json = serde_json::json!({
110 "id": 1,
111 "result": {
112 "changes": {
113 "file:///tmp/a.rs": [
114 { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 3 } }, "newText": "bar" }
115 ],
116 "file:///tmp/b.rs": [
117 { "range": { "start": { "line": 5, "character": 4 }, "end": { "line": 5, "character": 7 } }, "newText": "bar" },
118 { "range": { "start": { "line": 10, "character": 0 }, "end": { "line": 10, "character": 3 } }, "newText": "bar" }
119 ]
120 }
121 }
122 });
123 handle_rename_response(&json, &s);
124 let we = s.lock().unwrap().workspace_edit.clone();
125 assert_eq!(we.len(), 2);
126 assert_eq!(we.get(&PathBuf::from("/tmp/a.rs")).unwrap().len(), 1);
127 assert_eq!(we.get(&PathBuf::from("/tmp/b.rs")).unwrap().len(), 2);
128}
129
130#[test]
131fn handle_rename_document_changes() {
132 let s = make_state();
133 let json = serde_json::json!({
134 "id": 1,
135 "result": {
136 "documentChanges": [
137 {
138 "textDocument": { "uri": "file:///tmp/x.rs", "version": 2 },
139 "edits": [
140 { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 3 } }, "newText": "foo" }
141 ]
142 }
143 ]
144 }
145 });
146 handle_rename_response(&json, &s);
147 let we = s.lock().unwrap().workspace_edit.clone();
148 assert_eq!(we.len(), 1);
149 assert_eq!(we.get(&PathBuf::from("/tmp/x.rs")).unwrap().len(), 1);
150}
151
152#[test]
153fn handle_document_symbols_jerarquico() {
154 let s = make_state();
155 // Estructura: struct Foo { fn bar(), fn baz() } + fn top()
156 let json = serde_json::json!({
157 "id": 1,
158 "result": [
159 {
160 "name": "Foo",
161 "kind": 23, // struct
162 "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 10, "character": 1 } },
163 "selectionRange": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 10 } },
164 "children": [
165 {
166 "name": "bar",
167 "kind": 6, // method
168 "range": { "start": { "line": 2, "character": 4 }, "end": { "line": 4, "character": 5 } },
169 "selectionRange": { "start": { "line": 2, "character": 7 }, "end": { "line": 2, "character": 10 } }
170 },
171 {
172 "name": "baz",
173 "kind": 6,
174 "range": { "start": { "line": 6, "character": 4 }, "end": { "line": 8, "character": 5 } },
175 "selectionRange": { "start": { "line": 6, "character": 7 }, "end": { "line": 6, "character": 10 } }
176 }
177 ]
178 },
179 {
180 "name": "top",
181 "kind": 12, // function
182 "range": { "start": { "line": 12, "character": 0 }, "end": { "line": 14, "character": 1 } },
183 "selectionRange": { "start": { "line": 12, "character": 3 }, "end": { "line": 12, "character": 6 } }
184 }
185 ]
186 });
187 handle_document_symbols_response(&json, &s);
188 let syms = s.lock().unwrap().document_symbols.clone();
189 assert_eq!(syms.len(), 4, "esperaba 4 entradas flattening");
190
191 assert_eq!(syms[0].name, "Foo");
192 assert_eq!(syms[0].kind, "struct");
193 assert_eq!(syms[0].line, 0);
194 assert_eq!(syms[0].depth, 0);
195 assert_eq!(syms[0].container, None);
196
197 assert_eq!(syms[1].name, "bar");
198 assert_eq!(syms[1].kind, "method");
199 assert_eq!(syms[1].line, 2);
200 assert_eq!(syms[1].depth, 1);
201 assert_eq!(syms[1].container.as_deref(), Some("Foo"));
202
203 assert_eq!(syms[2].name, "baz");
204 assert_eq!(syms[2].depth, 1);
205 assert_eq!(syms[2].container.as_deref(), Some("Foo"));
206
207 assert_eq!(syms[3].name, "top");
208 assert_eq!(syms[3].kind, "fn");
209 assert_eq!(syms[3].depth, 0);
210}
211
212#[test]
213fn handle_document_symbols_legacy_symbolinformation() {
214 let s = make_state();
215 // Formato viejo: SymbolInformation[] (plano + location).
216 let json = serde_json::json!({
217 "id": 1,
218 "result": [
219 {
220 "name": "main",
221 "kind": 12,
222 "location": {
223 "uri": "file:///tmp/x.rs",
224 "range": { "start": { "line": 0, "character": 3 }, "end": { "line": 0, "character": 7 } }
225 }
226 },
227 {
228 "name": "helper",
229 "kind": 12,
230 "containerName": "main",
231 "location": {
232 "uri": "file:///tmp/x.rs",
233 "range": { "start": { "line": 5, "character": 3 }, "end": { "line": 5, "character": 9 } }
234 }
235 }
236 ]
237 });
238 handle_document_symbols_response(&json, &s);
239 let syms = s.lock().unwrap().document_symbols.clone();
240 assert_eq!(syms.len(), 2);
241 assert_eq!(syms[1].name, "helper");
242 assert_eq!(syms[1].container.as_deref(), Some("main"));
243}
244
245#[test]
246fn handle_references_response_array() {
247 let s = make_state();
248 let json = serde_json::json!({
249 "id": 1,
250 "result": [
251 { "uri": "file:///tmp/a.rs", "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 5 } } },
252 { "uri": "file:///tmp/b.rs", "range": { "start": { "line": 10, "character": 0 }, "end": { "line": 10, "character": 3 } } }
253 ]
254 });
255 handle_references_response(&json, &s);
256 let refs = s.lock().unwrap().references.clone();
257 assert_eq!(refs.len(), 2);
258 assert_eq!(refs[0].path, PathBuf::from("/tmp/a.rs"));
259 assert_eq!(refs[0].line, 1);
260 assert_eq!(refs[1].path, PathBuf::from("/tmp/b.rs"));
261 assert_eq!(refs[1].line, 10);
262}
263
264#[test]
265fn parse_signature_help_basic() {
266 let result = serde_json::json!({
267 "signatures": [{
268 "label": "fn foo(x: i32, y: String) -> u64",
269 "parameters": [
270 { "label": "x: i32" },
271 { "label": "y: String" }
272 ]
273 }],
274 "activeSignature": 0,
275 "activeParameter": 1
276 });
277 let info = parse_signature_help(&result).unwrap();
278 assert_eq!(info.label, "fn foo(x: i32, y: String) -> u64");
279 assert_eq!(info.active_param, 1);
280 assert_eq!(info.param_labels, vec!["x: i32", "y: String"]);
281}
282
283#[test]
284fn parse_signature_help_offset_label() {
285 // Label como [start, end] dentro del label de la firma.
286 let result = serde_json::json!({
287 "signatures": [{
288 "label": "foo(x, y)",
289 "parameters": [
290 { "label": [4, 5] },
291 { "label": [7, 8] }
292 ]
293 }]
294 });
295 let info = parse_signature_help(&result).unwrap();
296 assert_eq!(info.param_labels, vec!["x", "y"]);
297}
298
299#[test]
300fn parse_text_edit_basic() {
301 let v = serde_json::json!({
302 "range": {
303 "start": { "line": 1, "character": 0 },
304 "end": { "line": 1, "character": 4 }
305 },
306 "newText": "let "
307 });
308 let e = parse_text_edit(&v).unwrap();
309 assert_eq!(e.start_line, 1);
310 assert_eq!(e.start_col, 0);
311 assert_eq!(e.end_line, 1);
312 assert_eq!(e.end_col, 4);
313 assert_eq!(e.new_text, "let ");
314}
315
316#[test]
317fn handle_text_edits_response_array() {
318 let s = make_state();
319 let json = serde_json::json!({
320 "id": 1,
321 "result": [
322 { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 3 } }, "newText": "fn " },
323 { "range": { "start": { "line": 1, "character": 4 }, "end": { "line": 1, "character": 5 } }, "newText": "" }
324 ]
325 });
326 handle_text_edits_response(&json, &s);
327 let edits = s.lock().unwrap().text_edits.clone();
328 assert_eq!(edits.len(), 2);
329}
330
331#[test]
332fn handle_definition_location_simple() {
333 let s = make_state();
334 let json = serde_json::json!({
335 "id": 1,
336 "result": {
337 "uri": "file:///tmp/x.rs",
338 "range": {
339 "start": { "line": 10, "character": 4 },
340 "end": { "line": 10, "character": 9 }
341 }
342 }
343 });
344 handle_definition_response(&json, &s);
345 let d = s.lock().unwrap().definition.clone().unwrap();
346 assert_eq!(d.path, PathBuf::from("/tmp/x.rs"));
347 assert_eq!(d.line, 10);
348 assert_eq!(d.col, 4);
349}
350
351#[test]
352fn handle_definition_location_link_array() {
353 let s = make_state();
354 let json = serde_json::json!({
355 "id": 1,
356 "result": [
357 {
358 "targetUri": "file:///tmp/y.rs",
359 "targetSelectionRange": {
360 "start": { "line": 0, "character": 7 },
361 "end": { "line": 0, "character": 12 }
362 }
363 }
364 ]
365 });
366 handle_definition_response(&json, &s);
367 let d = s.lock().unwrap().definition.clone().unwrap();
368 assert_eq!(d.path, PathBuf::from("/tmp/y.rs"));
369 assert_eq!(d.line, 0);
370 assert_eq!(d.col, 7);
371}
372
373#[test]
374fn rust_analyzer_client_sin_binary_no_panic() {
375 // Si rust-analyzer no está instalado, el spawn falla en silencio
376 // y el client queda en modo no-op (state vacío).
377 let c = RustAnalyzerClient::with_command(PathBuf::from("/tmp"), "rust-analyzer-missing-99999");
378 // diagnostics() siempre devuelve vacío hasta que el server responde.
379 assert!(c.diagnostics(&PathBuf::from("/tmp/x")).is_empty());
380}