Files
llimphi/widgets/text-editor-lsp/src/tests.rs
T
sergio e65e9cc623 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>
2026-06-04 04:23:42 +00:00

381 lines
12 KiB
Rust

use super::*;
#[test]
fn noop_devuelve_vacio() {
let c = NoopLspClient;
assert!(c.diagnostics(&PathBuf::from("x")).is_empty());
}
#[test]
fn noop_no_panic_en_eventos() {
let mut c = NoopLspClient;
c.did_open(&PathBuf::from("x"), "rust", "fn main() {}");
c.did_change(&PathBuf::from("x"), "fn main() { 1 }");
c.did_close(&PathBuf::from("x"));
}
#[test]
fn parse_diagnostic_minimo() {
let json = serde_json::json!({
"range": {
"start": { "line": 3, "character": 5 },
"end": { "line": 3, "character": 12 }
},
"severity": 1,
"message": "no es así",
"source": "rustc"
});
let d = parse_lsp_diagnostic(&json).unwrap();
assert_eq!(d.range.start, Pos::new(3, 5));
assert_eq!(d.range.end, Pos::new(3, 12));
assert_eq!(d.severity, Severity::Error);
assert_eq!(d.message, "no es así");
assert_eq!(d.source.as_deref(), Some("rustc"));
}
#[test]
fn parse_diagnostic_sin_severidad_es_info() {
let json = serde_json::json!({
"range": {
"start": { "line": 0, "character": 0 },
"end": { "line": 0, "character": 1 }
},
"message": "x"
});
let d = parse_lsp_diagnostic(&json).unwrap();
assert_eq!(d.severity, Severity::Information);
}
#[test]
fn parse_completion_minimo() {
let v = serde_json::json!({
"label": "to_string",
"insertText": "to_string()",
"kind": 2,
"detail": "fn(&self) -> String"
});
let c = parse_completion(&v).unwrap();
assert_eq!(c.label, "to_string");
assert_eq!(c.insert_text.as_deref(), Some("to_string()"));
assert_eq!(c.kind.as_deref(), Some("Method"));
assert_eq!(c.detail.as_deref(), Some("fn(&self) -> String"));
}
#[test]
fn parse_hover_string_simple() {
let v = serde_json::json!({ "contents": "hola" });
let h = parse_hover(&v).unwrap();
assert_eq!(h.contents, "hola");
}
#[test]
fn parse_hover_marked_object() {
let v = serde_json::json!({
"contents": { "kind": "markdown", "value": "**fn**(x: i32) -> i32" }
});
let h = parse_hover(&v).unwrap();
assert_eq!(h.contents, "**fn**(x: i32) -> i32");
}
#[test]
fn parse_hover_array_concatena() {
let v = serde_json::json!({
"contents": ["primero", { "value": "segundo" }, ""]
});
let h = parse_hover(&v).unwrap();
assert_eq!(h.contents, "primero\nsegundo");
}
#[test]
fn parse_hover_vacio_devuelve_none() {
let v = serde_json::json!({ "contents": "" });
assert!(parse_hover(&v).is_none());
}
#[test]
fn parse_completion_sin_insert_text_usa_label() {
let v = serde_json::json!({ "label": "main" });
let c = parse_completion(&v).unwrap();
assert_eq!(c.text_to_insert(), "main");
}
fn make_state() -> SharedState {
Arc::new(Mutex::new(SharedInner::default()))
}
#[test]
fn handle_rename_changes_map() {
let s = make_state();
let json = serde_json::json!({
"id": 1,
"result": {
"changes": {
"file:///tmp/a.rs": [
{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 3 } }, "newText": "bar" }
],
"file:///tmp/b.rs": [
{ "range": { "start": { "line": 5, "character": 4 }, "end": { "line": 5, "character": 7 } }, "newText": "bar" },
{ "range": { "start": { "line": 10, "character": 0 }, "end": { "line": 10, "character": 3 } }, "newText": "bar" }
]
}
}
});
handle_rename_response(&json, &s);
let we = s.lock().unwrap().workspace_edit.clone();
assert_eq!(we.len(), 2);
assert_eq!(we.get(&PathBuf::from("/tmp/a.rs")).unwrap().len(), 1);
assert_eq!(we.get(&PathBuf::from("/tmp/b.rs")).unwrap().len(), 2);
}
#[test]
fn handle_rename_document_changes() {
let s = make_state();
let json = serde_json::json!({
"id": 1,
"result": {
"documentChanges": [
{
"textDocument": { "uri": "file:///tmp/x.rs", "version": 2 },
"edits": [
{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 3 } }, "newText": "foo" }
]
}
]
}
});
handle_rename_response(&json, &s);
let we = s.lock().unwrap().workspace_edit.clone();
assert_eq!(we.len(), 1);
assert_eq!(we.get(&PathBuf::from("/tmp/x.rs")).unwrap().len(), 1);
}
#[test]
fn handle_document_symbols_jerarquico() {
let s = make_state();
// Estructura: struct Foo { fn bar(), fn baz() } + fn top()
let json = serde_json::json!({
"id": 1,
"result": [
{
"name": "Foo",
"kind": 23, // struct
"range": { "start": { "line": 0, "character": 0 }, "end": { "line": 10, "character": 1 } },
"selectionRange": { "start": { "line": 0, "character": 7 }, "end": { "line": 0, "character": 10 } },
"children": [
{
"name": "bar",
"kind": 6, // method
"range": { "start": { "line": 2, "character": 4 }, "end": { "line": 4, "character": 5 } },
"selectionRange": { "start": { "line": 2, "character": 7 }, "end": { "line": 2, "character": 10 } }
},
{
"name": "baz",
"kind": 6,
"range": { "start": { "line": 6, "character": 4 }, "end": { "line": 8, "character": 5 } },
"selectionRange": { "start": { "line": 6, "character": 7 }, "end": { "line": 6, "character": 10 } }
}
]
},
{
"name": "top",
"kind": 12, // function
"range": { "start": { "line": 12, "character": 0 }, "end": { "line": 14, "character": 1 } },
"selectionRange": { "start": { "line": 12, "character": 3 }, "end": { "line": 12, "character": 6 } }
}
]
});
handle_document_symbols_response(&json, &s);
let syms = s.lock().unwrap().document_symbols.clone();
assert_eq!(syms.len(), 4, "esperaba 4 entradas flattening");
assert_eq!(syms[0].name, "Foo");
assert_eq!(syms[0].kind, "struct");
assert_eq!(syms[0].line, 0);
assert_eq!(syms[0].depth, 0);
assert_eq!(syms[0].container, None);
assert_eq!(syms[1].name, "bar");
assert_eq!(syms[1].kind, "method");
assert_eq!(syms[1].line, 2);
assert_eq!(syms[1].depth, 1);
assert_eq!(syms[1].container.as_deref(), Some("Foo"));
assert_eq!(syms[2].name, "baz");
assert_eq!(syms[2].depth, 1);
assert_eq!(syms[2].container.as_deref(), Some("Foo"));
assert_eq!(syms[3].name, "top");
assert_eq!(syms[3].kind, "fn");
assert_eq!(syms[3].depth, 0);
}
#[test]
fn handle_document_symbols_legacy_symbolinformation() {
let s = make_state();
// Formato viejo: SymbolInformation[] (plano + location).
let json = serde_json::json!({
"id": 1,
"result": [
{
"name": "main",
"kind": 12,
"location": {
"uri": "file:///tmp/x.rs",
"range": { "start": { "line": 0, "character": 3 }, "end": { "line": 0, "character": 7 } }
}
},
{
"name": "helper",
"kind": 12,
"containerName": "main",
"location": {
"uri": "file:///tmp/x.rs",
"range": { "start": { "line": 5, "character": 3 }, "end": { "line": 5, "character": 9 } }
}
}
]
});
handle_document_symbols_response(&json, &s);
let syms = s.lock().unwrap().document_symbols.clone();
assert_eq!(syms.len(), 2);
assert_eq!(syms[1].name, "helper");
assert_eq!(syms[1].container.as_deref(), Some("main"));
}
#[test]
fn handle_references_response_array() {
let s = make_state();
let json = serde_json::json!({
"id": 1,
"result": [
{ "uri": "file:///tmp/a.rs", "range": { "start": { "line": 1, "character": 2 }, "end": { "line": 1, "character": 5 } } },
{ "uri": "file:///tmp/b.rs", "range": { "start": { "line": 10, "character": 0 }, "end": { "line": 10, "character": 3 } } }
]
});
handle_references_response(&json, &s);
let refs = s.lock().unwrap().references.clone();
assert_eq!(refs.len(), 2);
assert_eq!(refs[0].path, PathBuf::from("/tmp/a.rs"));
assert_eq!(refs[0].line, 1);
assert_eq!(refs[1].path, PathBuf::from("/tmp/b.rs"));
assert_eq!(refs[1].line, 10);
}
#[test]
fn parse_signature_help_basic() {
let result = serde_json::json!({
"signatures": [{
"label": "fn foo(x: i32, y: String) -> u64",
"parameters": [
{ "label": "x: i32" },
{ "label": "y: String" }
]
}],
"activeSignature": 0,
"activeParameter": 1
});
let info = parse_signature_help(&result).unwrap();
assert_eq!(info.label, "fn foo(x: i32, y: String) -> u64");
assert_eq!(info.active_param, 1);
assert_eq!(info.param_labels, vec!["x: i32", "y: String"]);
}
#[test]
fn parse_signature_help_offset_label() {
// Label como [start, end] dentro del label de la firma.
let result = serde_json::json!({
"signatures": [{
"label": "foo(x, y)",
"parameters": [
{ "label": [4, 5] },
{ "label": [7, 8] }
]
}]
});
let info = parse_signature_help(&result).unwrap();
assert_eq!(info.param_labels, vec!["x", "y"]);
}
#[test]
fn parse_text_edit_basic() {
let v = serde_json::json!({
"range": {
"start": { "line": 1, "character": 0 },
"end": { "line": 1, "character": 4 }
},
"newText": "let "
});
let e = parse_text_edit(&v).unwrap();
assert_eq!(e.start_line, 1);
assert_eq!(e.start_col, 0);
assert_eq!(e.end_line, 1);
assert_eq!(e.end_col, 4);
assert_eq!(e.new_text, "let ");
}
#[test]
fn handle_text_edits_response_array() {
let s = make_state();
let json = serde_json::json!({
"id": 1,
"result": [
{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 3 } }, "newText": "fn " },
{ "range": { "start": { "line": 1, "character": 4 }, "end": { "line": 1, "character": 5 } }, "newText": "" }
]
});
handle_text_edits_response(&json, &s);
let edits = s.lock().unwrap().text_edits.clone();
assert_eq!(edits.len(), 2);
}
#[test]
fn handle_definition_location_simple() {
let s = make_state();
let json = serde_json::json!({
"id": 1,
"result": {
"uri": "file:///tmp/x.rs",
"range": {
"start": { "line": 10, "character": 4 },
"end": { "line": 10, "character": 9 }
}
}
});
handle_definition_response(&json, &s);
let d = s.lock().unwrap().definition.clone().unwrap();
assert_eq!(d.path, PathBuf::from("/tmp/x.rs"));
assert_eq!(d.line, 10);
assert_eq!(d.col, 4);
}
#[test]
fn handle_definition_location_link_array() {
let s = make_state();
let json = serde_json::json!({
"id": 1,
"result": [
{
"targetUri": "file:///tmp/y.rs",
"targetSelectionRange": {
"start": { "line": 0, "character": 7 },
"end": { "line": 0, "character": 12 }
}
}
]
});
handle_definition_response(&json, &s);
let d = s.lock().unwrap().definition.clone().unwrap();
assert_eq!(d.path, PathBuf::from("/tmp/y.rs"));
assert_eq!(d.line, 0);
assert_eq!(d.col, 7);
}
#[test]
fn rust_analyzer_client_sin_binary_no_panic() {
// Si rust-analyzer no está instalado, el spawn falla en silencio
// y el client queda en modo no-op (state vacío).
let c = RustAnalyzerClient::with_command(PathBuf::from("/tmp"), "rust-analyzer-missing-99999");
// diagnostics() siempre devuelve vacío hasta que el server responde.
assert!(c.diagnostics(&PathBuf::from("/tmp/x")).is_empty());
}