test(lsp): cover documented hover facts
Make hover token-position aware and add protocol/helper coverage for documented hemplate hover facts. req: diagnostics/007 req: diagnostics/008
This commit is contained in:
+94
-10
@@ -510,25 +510,29 @@ fn hemplate_hover(uri: &str, text: &str, position: Option<(usize, usize)>) -> se
|
|||||||
let line = position
|
let line = position
|
||||||
.and_then(|(line, _)| text.lines().nth(line))
|
.and_then(|(line, _)| text.lines().nth(line))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let value = if line.contains("data-hemx-slot") {
|
let character = position.map(|(_, character)| character).unwrap_or_default();
|
||||||
|
let at = |token: &str| token_at(line, character, token);
|
||||||
|
let value = if at("data-hemx-root") {
|
||||||
|
Some("`data-hemx-root` declares the root where the hemx runtime scopes delegated handlers and generated resources.")
|
||||||
|
} else if at("data-hemx-slot") {
|
||||||
Some("`data-hemx-slot` declares a generated partial target. Inside `h-for`, add template `h-key`; compiler diagnostics come from `hemx-build`.")
|
Some("`data-hemx-slot` declares a generated partial target. Inside `h-for`, add template `h-key`; compiler diagnostics come from `hemx-build`.")
|
||||||
} else if line.contains("data-hemx-form") {
|
} else if at("data-hemx-form") {
|
||||||
Some("`data-hemx-form` declares a generated form target wired through hemx build output.")
|
Some("`data-hemx-form` declares a generated form target wired through hemx build output.")
|
||||||
} else if line.contains("data-hemx-handle") {
|
} else if at("data-hemx-handle") {
|
||||||
Some("`data-hemx-handle` declares a generated handle target wired through hemx build output.")
|
Some("`data-hemx-handle` declares a generated handle target wired through hemx build output.")
|
||||||
} else if line.contains("h-key") {
|
} else if at("h-key") {
|
||||||
Some("`h-key` is the stable template key used by generated targets inside `h-for` loops.")
|
Some("`h-key` is the stable template key used by generated targets inside `h-for` loops.")
|
||||||
} else if line.contains("h-for") {
|
} else if at("h-for") {
|
||||||
Some("`h-for` repeats children from a Rust-shaped iterator expression; generated targets inside the loop require `h-key`.")
|
Some("`h-for` repeats children from a Rust-shaped iterator expression; generated targets inside the loop require `h-key`.")
|
||||||
} else if line.contains("h-if") {
|
} else if at("h-if") {
|
||||||
Some("`h-if` renders an element when a Rust-shaped condition is true.")
|
Some("`h-if` renders an element when a Rust-shaped condition is true.")
|
||||||
} else if line.contains("h-match") || line.contains("h-case") {
|
} else if at("h-match") || at("h-case") {
|
||||||
Some("`h-match`/`h-case` use Rust-shaped pattern arms; `h-case=\"_\"` is the default arm.")
|
Some("`h-match`/`h-case` use Rust-shaped pattern arms; `h-case=\"_\"` is the default arm.")
|
||||||
} else if line.contains("{+=") {
|
} else if at("{+=") {
|
||||||
Some("`{+= expr =+}` inserts trusted/rendered HTML. Prefer escaped `{+ expr +}` for user content.")
|
Some("`{+= expr =+}` inserts trusted/rendered HTML. Prefer escaped `{+ expr +}` for user content.")
|
||||||
} else if line.contains("{+") {
|
} else if at("{+") {
|
||||||
Some("`{+ expr +}` inserts escaped text.")
|
Some("`{+ expr +}` inserts escaped text.")
|
||||||
} else if line.contains('+') {
|
} else if at("+") {
|
||||||
Some("`+attr=\"expr\"` evaluates a dynamic HTML attribute expression.")
|
Some("`+attr=\"expr\"` evaluates a dynamic HTML attribute expression.")
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -541,6 +545,11 @@ fn hemplate_hover(uri: &str, text: &str, position: Option<(usize, usize)>) -> se
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn token_at(line: &str, character: usize, token: &str) -> bool {
|
||||||
|
line.match_indices(token)
|
||||||
|
.any(|(start, matched)| character >= start && character < start + matched.len())
|
||||||
|
}
|
||||||
|
|
||||||
fn hover_markdown(value: &str) -> serde_json::Value {
|
fn hover_markdown(value: &str) -> serde_json::Value {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"contents": {
|
"contents": {
|
||||||
@@ -826,6 +835,74 @@ mod tests {
|
|||||||
assert!(labels.contains("{+ expr +}"));
|
assert!(labels.contains("{+ expr +}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_items_cover_documented_hemplate_surface() {
|
||||||
|
// req: diagnostics/007 req: diagnostics/008
|
||||||
|
let text = r#"<main data-hemx-root="app"><template h-for="item in &self.items" h-key="item.id"><p +class="self.class_name">{+ self.title +}{+= trusted =+}</p></template></main>"#;
|
||||||
|
let cases = [
|
||||||
|
("data-hemx-root", "runtime scopes delegated handlers"),
|
||||||
|
("h-for", "repeats children"),
|
||||||
|
("h-key", "stable template key"),
|
||||||
|
("+class", "dynamic HTML attribute"),
|
||||||
|
("{+ self.title +}", "escaped text"),
|
||||||
|
("{+= trusted =+}", "trusted/rendered HTML"),
|
||||||
|
];
|
||||||
|
for (needle, expected) in cases {
|
||||||
|
let character = text.find(needle).expect("fixture needle");
|
||||||
|
let hover = hemplate_hover("file:///tmp/hover.heml", text, Some((0, character)));
|
||||||
|
let value = hover_markdown_value(&hover);
|
||||||
|
assert!(
|
||||||
|
value.contains(expected),
|
||||||
|
"hover for `{needle}` should mention `{expected}`, got {value:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
value.contains("docs/hemplate-syntax.md"),
|
||||||
|
"hover for `{needle}` should cite syntax docs, got {value:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
hemplate_hover("file:///tmp/hover.heml", text, Some((0, 0))),
|
||||||
|
serde_json::Value::Null,
|
||||||
|
"ordinary HTML text is left to normal editor tooling"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lsp_hover_returns_documented_markdown() {
|
||||||
|
// req: diagnostics/007 req: diagnostics/008
|
||||||
|
let text = r#"<main data-hemx-root="app"><template h-for="item in &self.items" h-key="item.id"><p>{+ item.title +}</p></template></main>"#;
|
||||||
|
let uri = "file:///tmp/hover.heml";
|
||||||
|
let character = text.find("h-key").expect("h-key position");
|
||||||
|
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/hover",
|
||||||
|
"params": {"textDocument": {"uri": uri}, "position": {"line": 0, "character": character}}
|
||||||
|
})),
|
||||||
|
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 hover = messages
|
||||||
|
.iter()
|
||||||
|
.find(|message| message["id"] == 2)
|
||||||
|
.expect("hover response");
|
||||||
|
let value = hover_markdown_value(&hover["result"]);
|
||||||
|
assert!(value.contains("stable template key"));
|
||||||
|
assert!(value.contains("docs/hemplate-syntax.md"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workout_template_fields_are_available_from_repo_facts() {
|
fn workout_template_fields_are_available_from_repo_facts() {
|
||||||
// req: diagnostics/006
|
// req: diagnostics/006
|
||||||
@@ -1034,6 +1111,13 @@ mod tests {
|
|||||||
assert!(output.contains("docs/hemplate-syntax.md"));
|
assert!(output.contains("docs/hemplate-syntax.md"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hover_markdown_value(hover: &serde_json::Value) -> String {
|
||||||
|
hover["contents"]["value"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
fn lsp_messages(output: &[u8]) -> Vec<serde_json::Value> {
|
fn lsp_messages(output: &[u8]) -> Vec<serde_json::Value> {
|
||||||
let mut bytes = output;
|
let mut bytes = output;
|
||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user