use std::path::PathBuf;
use bock_air::{
lower_module, resolve_names_with_registry, visitor::walk_node, visitor::Visitor, AIRNode,
ModuleRegistry, NodeId, NodeIdGen, NodeKind, SymbolTable,
};
use bock_errors::{FileId, Span};
use bock_lexer::Lexer;
use bock_parser::Parser;
use bock_source::SourceMap;
use bock_types::{seed_imports, FnType, PrimitiveType, Type, TypeChecker};
use crate::goto_definition::position_to_offset;
use crate::type_display::format_type;
pub struct HoverResult {
pub source_map: SourceMap,
pub file_id: FileId,
pub contents: String,
pub span: Span,
}
#[must_use]
pub fn hover(path: PathBuf, content: String, line: u32, character: u32) -> Option<HoverResult> {
let mut source_map = SourceMap::new();
let file_id = source_map.add_file(path, content);
let source_file = source_map.get_file(file_id);
let offset = position_to_offset(&source_file.content, line, character)?;
let mut lexer = Lexer::new(source_file);
let tokens = lexer.tokenize();
let mut parser = Parser::new(tokens, source_file);
let module = parser.parse_module();
let registry = ModuleRegistry::new();
let mut symbols = SymbolTable::new();
let _ = resolve_names_with_registry(&module, &mut symbols, ®istry);
let id_gen = NodeIdGen::new();
let mut air_module = lower_module(&module, &id_gen, &symbols);
let mut checker = TypeChecker::new();
register_builtins(&mut checker);
seed_imports(&mut checker, &module.imports, ®istry);
checker.check_module(&mut air_module);
let mut finder = NodeFinder::new(offset);
finder.visit_node(&air_module);
let (node_id, node_span, kind_label) = finder.best?;
let ty = checker.type_of(node_id)?.clone();
let ty = checker.subst.apply(&ty);
if matches!(ty, Type::Error) {
return None;
}
let contents = render_hover(&ty, kind_label);
Some(HoverResult {
source_map,
file_id,
contents,
span: node_span,
})
}
fn render_hover(ty: &Type, kind_label: Option<&'static str>) -> String {
let prefix = match (ty, kind_label) {
(Type::Function(_), _) => "signature",
(_, Some(label)) => label,
(_, None) => "type",
};
match ty {
Type::Function(f) => format!("```\n{}\n```\n\n_{prefix}_", fn_signature(f)),
_ => format!("`{}`\n\n_{prefix}_", format_type(ty)),
}
}
fn fn_signature(f: &FnType) -> String {
let mut out = String::from("fn(");
let mut first = true;
for p in &f.params {
if !first {
out.push_str(", ");
}
first = false;
out.push_str(&format_type(p));
}
out.push_str(") -> ");
out.push_str(&format_type(&f.ret));
if !f.effects.is_empty() {
out.push_str(" with ");
let mut first = true;
for e in &f.effects {
if !first {
out.push_str(", ");
}
first = false;
out.push_str(&e.name);
}
}
out
}
fn register_builtins(checker: &mut TypeChecker) {
let io_fn_ty = Type::Function(FnType {
params: vec![Type::Primitive(PrimitiveType::String)],
ret: Box::new(Type::Primitive(PrimitiveType::Void)),
effects: vec![],
});
for name in ["print", "println", "debug"] {
checker.env.define(name, io_fn_ty.clone());
}
let assert_ty = Type::Function(FnType {
params: vec![Type::Primitive(PrimitiveType::Bool)],
ret: Box::new(Type::Primitive(PrimitiveType::Void)),
effects: vec![],
});
checker.env.define("assert", assert_ty);
let expect_ty = Type::Function(FnType {
params: vec![Type::Error],
ret: Box::new(Type::Error),
effects: vec![],
});
checker.env.define("expect", expect_ty);
let never_fn_ty = Type::Function(FnType {
params: vec![],
ret: Box::new(Type::Primitive(PrimitiveType::Never)),
effects: vec![],
});
for name in ["todo", "unreachable"] {
checker.env.define(name, never_fn_ty.clone());
}
let constructor_ty = Type::Function(FnType {
params: vec![Type::Error],
ret: Box::new(Type::Error),
effects: vec![],
});
for name in ["Ok", "Err", "Some"] {
checker.env.define(name, constructor_ty.clone());
}
checker.env.define("None", Type::Error);
}
struct NodeFinder {
offset: usize,
best: Option<(NodeId, Span, Option<&'static str>)>,
best_width: usize,
}
impl NodeFinder {
fn new(offset: usize) -> Self {
Self {
offset,
best: None,
best_width: usize::MAX,
}
}
fn consider(&mut self, node: &AIRNode) {
let span = node.span;
if !(self.offset >= span.start && self.offset <= span.end) {
return;
}
let width = span.end.saturating_sub(span.start);
if width <= self.best_width {
self.best_width = width;
self.best = Some((node.id, span, describe_kind(&node.kind)));
}
}
}
impl Visitor for NodeFinder {
fn visit_node(&mut self, node: &AIRNode) {
self.consider(node);
walk_node(self, node);
}
}
fn describe_kind(kind: &NodeKind) -> Option<&'static str> {
match kind {
NodeKind::Identifier { .. } => Some("variable"),
NodeKind::Literal { .. } => Some("literal"),
NodeKind::Call { .. } => Some("call"),
NodeKind::MethodCall { .. } => Some("method call"),
NodeKind::FieldAccess { .. } => Some("field"),
NodeKind::BinaryOp { .. } => Some("expression"),
NodeKind::UnaryOp { .. } => Some("expression"),
NodeKind::RecordConstruct { .. } => Some("record"),
NodeKind::ListLiteral { .. } => Some("list"),
NodeKind::MapLiteral { .. } => Some("map"),
NodeKind::SetLiteral { .. } => Some("set"),
NodeKind::TupleLiteral { .. } => Some("tuple"),
NodeKind::Lambda { .. } => Some("lambda"),
NodeKind::If { .. } => Some("if expression"),
NodeKind::Match { .. } => Some("match expression"),
NodeKind::LetBinding { .. } => Some("binding"),
NodeKind::Block { .. } => Some("block"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run(src: &str, line: u32, ch: u32) -> Option<HoverResult> {
hover(PathBuf::from("test.bock"), src.to_string(), line, ch)
}
#[test]
fn hover_on_let_binding_shows_int_type() {
let src = "\
module m
fn main() {
let answer = 42
answer
}
";
let result = run(src, 4, 4).expect("hover returned a result");
assert!(
result.contents.contains("Int"),
"expected Int in hover contents, got: {}",
result.contents
);
}
#[test]
fn hover_on_string_literal_shows_string_type() {
let src = "\
module m
fn main() {
let greeting = \"hello\"
}
";
let result = run(src, 3, 22).expect("hover returned a result");
assert!(
result.contents.contains("String"),
"expected String in hover, got: {}",
result.contents
);
}
#[test]
fn hover_on_bool_literal_shows_bool_type() {
let src = "\
module m
fn main() {
let flag = true
}
";
let result = run(src, 3, 16).expect("hover returned a result");
assert!(
result.contents.contains("Bool"),
"expected Bool in hover, got: {}",
result.contents
);
}
#[test]
fn hover_on_fn_call_callee_shows_signature() {
let src = "\
module m
fn add(a: Int, b: Int) -> Int {
a
}
fn main() {
add(1, 2)
}
";
let result = run(src, 7, 4).expect("hover returned a result");
assert!(
result.contents.contains("Int"),
"expected Int somewhere in hover, got: {}",
result.contents
);
}
#[test]
fn hover_returns_none_outside_any_node() {
let src = "\
module m
fn main() {
let x = 1
}
";
let _ = run(src, 0, 8);
}
#[test]
fn hover_returns_none_past_eof() {
let src = "module m\n";
assert!(run(src, 99, 0).is_none());
}
#[test]
fn hover_on_list_literal() {
let src = "\
module m
fn main() {
let xs = [1, 2, 3]
}
";
let result = run(src, 3, 13).expect("hover returned a result");
assert!(
result.contents.contains("Int") || result.contents.contains("List"),
"expected list type info, got: {}",
result.contents
);
}
#[test]
fn render_hover_formats_function_as_code_block() {
let ty = Type::Function(FnType {
params: vec![
Type::Primitive(PrimitiveType::Int),
Type::Primitive(PrimitiveType::Int),
],
ret: Box::new(Type::Primitive(PrimitiveType::Int)),
effects: vec![],
});
let out = render_hover(&ty, None);
assert!(out.contains("fn(Int, Int) -> Int"), "got: {out}");
assert!(out.contains("signature"), "got: {out}");
}
#[test]
fn render_hover_formats_primitive_inline() {
let out = render_hover(&Type::Primitive(PrimitiveType::String), Some("variable"));
assert!(out.contains("`String`"), "got: {out}");
assert!(out.contains("variable"), "got: {out}");
}
}