feat(lsp): explain generated targets on hover

This commit is contained in:
slhx agent
2026-07-13 11:03:03 +02:00
parent 8fd333090d
commit f93051357a
4 changed files with 59 additions and 3 deletions
+52 -1
View File
@@ -442,7 +442,10 @@ fn hemplate_completion_items(
items.push(completion_item(
&format!("ui::{}", target.name),
&format!("ui::{}", target.name),
"Generated hemx target discovered by hemx-build in the open `.heml` document.",
&format!(
"Generated hemx {} target discovered by hemx-build in the open `.heml` document.",
target.kind
),
));
}
}
@@ -502,6 +505,21 @@ fn completion_item(label: &str, insert_text: &str, detail: &str) -> serde_json::
}
fn hemplate_hover(uri: &str, text: &str, position: Option<(usize, usize)>) -> serde_json::Value {
if let Some((kind, name)) = position.and_then(|position| generated_target_at(text, position)) {
if let Ok(targets) =
hemx_build::generated_targets_for_heml_source(path_from_file_uri(uri), text)
{
if targets
.iter()
.any(|target| target.kind == kind && target.name == name)
{
return hover_markdown(&format!(
"Generated hemx `{kind}` target `{name}`.\n\nRust symbol: `ui::{name}`.\n\nSource: hemx-build facts for the current `.heml` document."
));
}
}
}
if let Some((owner, field)) = position.and_then(|position| dotted_name_at(text, position)) {
if let Ok(Some(facts)) =
hemx_build::template_context_facts_for_heml_source(path_from_file_uri(uri), text)
@@ -584,6 +602,26 @@ fn line_prefix(text: &str, position: (usize, usize)) -> Option<String> {
Some(line.chars().take(position.1).collect())
}
fn generated_target_at(text: &str, position: (usize, usize)) -> Option<(String, String)> {
let line = text.lines().nth(position.0)?;
let cursor = position.1.min(line.len());
[
("root", "data-hemx-root=\""),
("slot", "data-hemx-slot=\""),
("form", "data-hemx-form=\""),
("handle", "data-hemx-handle=\""),
]
.into_iter()
.find_map(|(kind, prefix)| {
line.match_indices(prefix).find_map(|(attribute_start, _)| {
let value_start = attribute_start + prefix.len();
let value_end = value_start + line[value_start..].find('"')?;
(cursor >= value_start && cursor <= value_end)
.then(|| (kind.to_owned(), line[value_start..value_end].to_owned()))
})
})
}
fn dotted_name_at(text: &str, position: (usize, usize)) -> Option<(String, String)> {
let line = text.lines().nth(position.0)?;
let chars = line.chars().collect::<Vec<_>>();
@@ -1083,6 +1121,19 @@ mod tests {
);
}
#[test]
fn hover_reports_generated_target_kind_and_rust_symbol() {
// req: diag/010
let uri = "file:///tmp/workout.heml";
let text = r#"<main data-hemx-root="workout"><section data-hemx-slot="progress_panel"></section></main>"#;
let cursor = text.find("progress_panel").expect("fixture target") + 2;
let hover = hemplate_hover(uri, text, Some((0, cursor))).to_string();
assert!(hover.contains("`slot` target `progress_panel`"), "{hover}");
assert!(hover.contains("`ui::progress_panel`"), "{hover}");
}
#[test]
fn source_positions_use_lsp_utf16_columns() {
assert_eq!(