use plg_shared::{StringInterner, Term, builtins};
use tower_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind, Position};
use crate::buffer::{parse_best_effort, predicate_indicator, word_at_position};
pub fn compute(content: &str, position: Position) -> Option<Hover> {
let word = word_at_position(content, position)?;
if word.is_empty() {
return None;
}
let body = if let Some(doc) = builtins::doc(&word) {
Some(format!("**`{word}`** (built-in)\n\n{doc}"))
} else {
lookup_user_clause_head(content, &word)
.map(|head| format!("**`{word}`** (user-defined)\n\n```prolog\n{head}\n```"))
};
body.map(|value| Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value,
}),
range: None,
})
}
fn lookup_user_clause_head(content: &str, name: &str) -> Option<String> {
let (clauses, _, interner) = parse_best_effort(content)?;
for clause in &clauses {
if let Some((n, _)) = predicate_indicator(&clause.head, &interner)
&& n == name
{
return Some(render_head(&clause.head, &interner));
}
}
None
}
fn render_head(head: &Term, interner: &StringInterner) -> String {
match head {
Term::Atom(id) => interner.resolve(*id).to_string(),
Term::Compound { functor, args } => {
let name = interner.resolve(*functor);
let argv: Vec<String> = args.iter().map(|a| render_arg(a, interner)).collect();
format!("{}({})", name, argv.join(", "))
}
_ => "<non-callable head>".to_string(),
}
}
fn render_arg(term: &Term, interner: &StringInterner) -> String {
match term {
Term::Atom(id) => interner.resolve(*id).to_string(),
Term::Integer(n) => n.to_string(),
Term::Float(f) => f.to_string(),
Term::Var(id) => format!("_{id}"),
Term::Compound { functor, args } => {
format!("{}/{}", interner.resolve(*functor), args.len())
}
Term::List { .. } => "[...]".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pos(line: u32, col: u32) -> Position {
Position {
line,
character: col,
}
}
#[test]
fn word_under_cursor_in_middle() {
let w = word_at_position("hello world", pos(0, 8));
assert_eq!(w.as_deref(), Some("world"));
}
#[test]
fn word_under_cursor_at_start() {
let w = word_at_position("findall(X, p(X), L)", pos(0, 0));
assert_eq!(w.as_deref(), Some("findall"));
}
#[test]
fn hover_on_builtin_shows_doc() {
let h = compute("p :- findall(X, q(X), L).\n", pos(0, 8)).unwrap();
match h.contents {
HoverContents::Markup(m) => {
assert!(m.value.contains("findall"));
assert!(m.value.contains("built-in"));
}
_ => panic!("expected markup"),
}
}
#[test]
fn hover_on_user_predicate_shows_clause_head() {
let src = "violation(F, sensitive_field) :- field(F).\np :- violation(X, R).\n";
let h = compute(src, pos(1, 6)).unwrap();
match h.contents {
HoverContents::Markup(m) => {
assert!(m.value.contains("violation"));
assert!(m.value.contains("user-defined"));
}
_ => panic!("expected markup"),
}
}
#[test]
fn hover_outside_a_word_returns_none() {
assert!(compute("foo bar", pos(0, 3)).is_none());
}
#[test]
fn hover_on_unknown_word_returns_none() {
assert!(compute("zzzxyz nothing here", pos(0, 0)).is_none());
}
#[test]
fn hover_on_between_shows_doc() {
let h = compute("p :- between(1, 3, X).\n", pos(0, 8)).unwrap();
match h.contents {
HoverContents::Markup(m) => assert!(m.value.contains("between")),
_ => panic!("expected markup"),
}
}
#[test]
fn word_at_position_with_multibyte_char_before_cursor() {
let content = "% é\nfindall(X, p(X), L)";
let w = word_at_position(content, pos(1, 5));
assert_eq!(w.as_deref(), Some("findall"));
}
}