From baf4b1529c493c4d883858da93c9517b99d72d8b Mon Sep 17 00:00:00 2001 From: slhx agent Date: Thu, 25 Jun 2026 23:29:33 +0200 Subject: [PATCH] test(lsp): cover documented completions Add protocol and helper coverage for documented hemplate completion items, including data-hemx-root. req: diagnostics/006 --- hemx-lsp/src/main.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/hemx-lsp/src/main.rs b/hemx-lsp/src/main.rs index 8d046b5..fb912f7 100644 --- a/hemx-lsp/src/main.rs +++ b/hemx-lsp/src/main.rs @@ -394,6 +394,11 @@ fn hemplate_completion_items( "{+= trusted_html =+}", "Trusted/rendered HTML expression; prefer escaped `{+ expr +}` for user content.", ), + completion_item( + "data-hemx-root", + "data-hemx-root=\"app\"", + "Generated hemx root for delegated runtime behavior.", + ), completion_item( "data-hemx-slot", "data-hemx-slot=\"content\"", @@ -746,6 +751,81 @@ mod tests { assert!(!outside_hover.to_string().contains("exercise.name: String")); } + #[test] + fn completion_items_cover_documented_hemplate_surface() { + // req: diagnostics/006 + let text = r#"
"#; + let items = hemplate_completion_items("file:///tmp/app.heml", text, Some((0, 6))); + let labels = items + .iter() + .filter_map(|item| item["label"].as_str()) + .collect::>(); + for label in [ + "h-for", + "h-key", + "h-if", + "h-match", + "h-case", + "+attr", + "{+ expr +}", + "{+= expr =+}", + "data-hemx-root", + "data-hemx-slot", + "data-hemx-form", + "data-hemx-handle", + ] { + assert!(labels.contains(label), "missing completion item `{label}`"); + } + assert!(items.iter().all(|item| item["documentation"]["value"] + .as_str() + .unwrap_or_default() + .contains("docs/hemplate-syntax.md"))); + } + + #[test] + fn lsp_completion_returns_documented_items() { + // req: diagnostics/006 + let text = r#"
{+ self.title +}
"#; + let uri = "file:///tmp/completion.heml"; + let input = [ + lsp_message(serde_json::json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}})), + lsp_message(serde_json::json!({ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": {"textDocument": {"uri": uri, "languageId": "heml", "version": 1, "text": text}} + })), + lsp_message(serde_json::json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/completion", + "params": {"textDocument": {"uri": uri}, "position": {"line": 0, "character": 6}} + })), + lsp_message(serde_json::json!({"jsonrpc": "2.0", "method": "exit"})), + ] + .join(""); + let mut reader = std::io::BufReader::new(input.as_bytes()); + let mut output = Vec::new(); + + serve_lsp(&mut reader, &mut output).expect("serve LSP"); + let messages = lsp_messages(&output); + let completion = messages + .iter() + .find(|message| message["id"] == 2) + .expect("completion response"); + let completion_items = completion["result"] + .as_array() + .or_else(|| completion["result"]["items"].as_array()) + .expect("completion items"); + let labels = completion_items + .iter() + .filter_map(|item| item["label"].as_str()) + .collect::>(); + assert!(labels.contains("h-if")); + assert!(labels.contains("data-hemx-root")); + assert!(labels.contains("data-hemx-slot")); + assert!(labels.contains("{+ expr +}")); + } + #[test] fn workout_template_fields_are_available_from_repo_facts() { // req: diagnostics/006