use super::schema;
use tower_lsp_server::ls_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position};
pub fn get_toml_hover(text: &str, position: Position) -> Option<Hover> {
let line_str = text.lines().nth(position.line as usize)?;
if let Some(section_name) = parse_section_header(line_str.trim()) {
let base_name = section_name.split('.').next().unwrap_or(§ion_name);
let section = schema::find_section(base_name)?;
let mut content = format!("## `[{}]`\n\n{}", section.name, section.description);
if !section.keys.is_empty() {
content.push_str("\n\n**Keys:**\n");
for key in section.keys {
let required = if key.required { " (required)" } else { "" };
content.push_str(&format!(
"\n- `{}`: {} — {}{}",
key.name,
key.value_type.display_name(),
key.description,
required,
));
}
}
if section.is_free_form {
content.push_str("\n\nThis section accepts arbitrary key-value pairs.");
}
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: content,
}),
range: None,
});
}
let trimmed = line_str.trim();
if let Some(eq_pos) = trimmed.find('=') {
let key_name = trimmed[..eq_pos].trim();
let col = position.character as usize;
let key_start = line_str.find(key_name).unwrap_or(0);
let key_end = key_start + key_name.len();
if col > key_end {
return None;
}
let section_name = current_section(text, position.line)?;
let base_section = section_name.split('.').next().unwrap_or(§ion_name);
let key_def = schema::find_key(base_section, key_name)?;
let required = if key_def.required {
"**Required**"
} else {
"Optional"
};
let mut content = format!(
"## `{}.{}`\n\n{}\n\n- **Type**: `{}`\n- **Status**: {}",
base_section,
key_def.name,
key_def.description,
key_def.value_type.display_name(),
required,
);
if !key_def.known_values.is_empty() {
content.push_str(&format!(
"\n- **Allowed values**: {}",
key_def
.known_values
.iter()
.map(|v| format!("`{}`", v))
.collect::<Vec<_>>()
.join(", ")
));
}
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: content,
}),
range: None,
});
}
None
}
fn parse_section_header(line: &str) -> Option<String> {
let trimmed = line.trim();
if trimmed.starts_with("[[") && trimmed.ends_with("]]") {
let inner = trimmed.trim_start_matches('[').trim_end_matches(']').trim();
Some(inner.to_string())
} else if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.starts_with("[[") {
let inner = trimmed.trim_start_matches('[').trim_end_matches(']').trim();
Some(inner.to_string())
} else {
None
}
}
fn current_section(text: &str, line: u32) -> Option<String> {
let lines: Vec<&str> = text.lines().collect();
for l in (0..=line as usize).rev() {
if let Some(line_text) = lines.get(l) {
if let Some(name) = parse_section_header(line_text.trim()) {
return Some(name);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hover_on_section_header() {
let text = "[project]\nname = \"test\"\n";
let hover = get_toml_hover(
text,
Position {
line: 0,
character: 3,
},
);
assert!(hover.is_some());
let h = hover.unwrap();
if let HoverContents::Markup(m) = h.contents {
assert!(m.value.contains("[project]"));
assert!(m.value.contains("Project metadata"));
} else {
panic!("Expected markup content");
}
}
#[test]
fn test_hover_on_key() {
let text = "[project]\nname = \"test\"\n";
let hover = get_toml_hover(
text,
Position {
line: 1,
character: 1,
},
);
assert!(hover.is_some());
let h = hover.unwrap();
if let HoverContents::Markup(m) = h.contents {
assert!(m.value.contains("project.name"));
assert!(m.value.contains("Required"));
} else {
panic!("Expected markup content");
}
}
#[test]
fn test_hover_on_value_returns_none() {
let text = "[project]\nname = \"test\"\n";
let hover = get_toml_hover(
text,
Position {
line: 1,
character: 10,
},
);
assert!(hover.is_none());
}
#[test]
fn test_hover_on_build_target() {
let text = "[build]\ntarget = \"bytecode\"\n";
let hover = get_toml_hover(
text,
Position {
line: 1,
character: 2,
},
);
assert!(hover.is_some());
let h = hover.unwrap();
if let HoverContents::Markup(m) = h.contents {
assert!(m.value.contains("build.target"));
assert!(m.value.contains("bytecode"));
assert!(m.value.contains("native"));
} else {
panic!("Expected markup content");
}
}
#[test]
fn test_hover_on_array_table_header() {
let text = "[[extensions]]\nname = \"duckdb\"\n";
let hover = get_toml_hover(
text,
Position {
line: 0,
character: 5,
},
);
assert!(hover.is_some());
let h = hover.unwrap();
if let HoverContents::Markup(m) = h.contents {
assert!(m.value.contains("[extensions]"));
assert!(m.value.contains("Extension module"));
} else {
panic!("Expected markup content");
}
}
#[test]
fn test_hover_on_unknown_section() {
let text = "[unknown]\nfoo = \"bar\"\n";
let hover = get_toml_hover(
text,
Position {
line: 0,
character: 3,
},
);
assert!(hover.is_none());
}
#[test]
fn test_hover_on_optional_key() {
let text = "[project]\nlicense = \"MIT\"\n";
let hover = get_toml_hover(
text,
Position {
line: 1,
character: 2,
},
);
assert!(hover.is_some());
let h = hover.unwrap();
if let HoverContents::Markup(m) = h.contents {
assert!(m.value.contains("Optional"));
assert!(!m.value.contains("**Required**"));
} else {
panic!("Expected markup content");
}
}
}