e65e9cc623
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>
381 lines
12 KiB
Rust
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());
|
|
}
|