use bynk_check::locals::{LocalBinding, LocalKind, binding_at_def, locals_at};
use bynk_syntax::lexer::{self, TokenKind};
use bynk_syntax::span::Span;
fn ident_at(text: &str, offset: usize) -> Option<(&str, Span)> {
let toks = lexer::tokenize_expanding_holes(text).ok()?;
toks.into_iter()
.find(|t| t.kind == TokenKind::Ident && t.span.start <= offset && offset <= t.span.end)
.map(|t| (&text[t.span.start..t.span.end], t.span))
}
fn target_at<'a>(
locals: &'a [LocalBinding],
text: &str,
offset: usize,
) -> Option<&'a LocalBinding> {
let (name, _) = ident_at(text, offset)?;
binding_at_def(locals, offset)
.filter(|b| b.name == name)
.or_else(|| {
locals_at(locals, offset)
.into_iter()
.find(|b| b.name == name)
})
}
pub fn local_sites_at(locals: &[LocalBinding], text: &str, offset: usize) -> Option<Vec<Span>> {
let target = target_at(locals, text, offset)?;
let toks = lexer::tokenize_expanding_holes(text).ok()?;
let mut sites = vec![target.def_span];
for t in &toks {
if t.kind != TokenKind::Ident || text[t.span.start..t.span.end] != target.name {
continue;
}
if t.span == target.def_span {
continue; }
if locals.iter().any(|b| b.def_span == t.span) {
continue;
}
if t.span.start < target.scope.start || t.span.end > target.scope.end {
continue; }
let resolves = locals_at(locals, t.span.start)
.into_iter()
.find(|b| b.name == target.name)
.map(|b| b.def_span);
if resolves == Some(target.def_span) {
sites.push(t.span);
}
}
Some(sites)
}
pub fn local_definition_at(locals: &[LocalBinding], text: &str, offset: usize) -> Option<Span> {
target_at(locals, text, offset).map(|b| b.def_span)
}
pub fn describe_local_at(locals: &[LocalBinding], text: &str, offset: usize) -> Option<String> {
let b = target_at(locals, text, offset)?;
let keyword = match b.kind {
LocalKind::Let => "let",
LocalKind::Param => "param",
};
Some(format!("```bynk\n{keyword} {}: {}\n```", b.name, b.ty))
}
pub fn local_token_sites(locals: &[LocalBinding], text: &str) -> Vec<(Span, bool)> {
let Ok(toks) = lexer::tokenize_expanding_holes(text) else {
return Vec::new();
};
let mut out = Vec::new();
for t in &toks {
if t.kind != TokenKind::Ident {
continue;
}
let name = &text[t.span.start..t.span.end];
if locals.iter().any(|b| b.def_span == t.span) {
out.push((t.span, true)); } else if locals_at(locals, t.span.start)
.into_iter()
.any(|b| b.name == name)
{
out.push((t.span, false)); }
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn bindings() -> Vec<LocalBinding> {
vec![
LocalBinding {
name: "n".into(),
def_span: Span { start: 5, end: 6 },
kind: LocalKind::Param,
ty: "Int".into(),
scope: Span { start: 20, end: 60 },
},
LocalBinding {
name: "x".into(),
def_span: Span { start: 26, end: 27 },
kind: LocalKind::Let,
ty: "Int".into(),
scope: Span { start: 34, end: 60 },
},
]
}
const TEXT: &str = "fn f(n: Int) -> Int { let x = n\n x + x\n}";
#[test]
fn sites_for_a_use_collect_def_plus_uses() {
let locals = bindings();
let x_use = TEXT.match_indices('x').nth(1).unwrap().0; let sites = local_sites_at(&locals, TEXT, x_use).expect("on a local");
assert!(
sites.contains(&Span { start: 26, end: 27 }),
"includes def: {sites:?}"
);
assert!(sites.len() >= 2, "def + at least one use: {sites:?}");
}
#[test]
fn definition_resolves_from_a_use() {
let locals = bindings();
let n_use = TEXT.rfind('n').unwrap(); assert_eq!(
local_definition_at(&locals, TEXT, n_use),
Some(Span { start: 5, end: 6 })
);
}
#[test]
fn not_on_a_local_yields_none() {
let locals = bindings();
assert!(local_sites_at(&locals, TEXT, 0).is_none()); }
#[test]
fn token_sites_mark_definitions_and_uses() {
let sites = local_token_sites(&bindings(), TEXT);
assert!(
sites.iter().any(|(_, decl)| *decl),
"has a definition token"
);
assert!(sites.iter().any(|(_, decl)| !*decl), "has a use token");
assert!(
sites.contains(&(Span { start: 26, end: 27 }, true)),
"x def is a declaration: {sites:?}"
);
}
#[test]
fn resolves_a_real_local_from_diagnose_project() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../bynkc/tests/fixtures/inlay/clean/src");
let r = bynk_ide::diagnose_project(&root, &std::collections::HashMap::new());
let file = r
.files
.iter()
.find(|f| f.source_path.to_string_lossy().ends_with("util.bynk"))
.expect("util.bynk analysed");
let text = &file.text;
let locals = r
.locals
.iter()
.find(|(p, _)| p.to_string_lossy().ends_with("util.bynk"))
.map(|(_, l)| l.clone())
.expect("util.bynk locals");
let use_off = text.rfind("total").expect("total use");
let sites = local_sites_at(&locals, text, use_off).expect("on a local");
assert!(sites.len() >= 2, "def + use: {sites:?}");
let def = text.find("total").expect("total def");
assert_eq!(sites[0].start, def, "def first");
}
#[test]
fn describe_local_renders_kind_and_type() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../bynkc/tests/fixtures/inlay/clean/src");
let r = bynk_ide::diagnose_project(&root, &std::collections::HashMap::new());
let file = r
.files
.iter()
.find(|f| f.source_path.to_string_lossy().ends_with("util.bynk"))
.expect("util.bynk analysed");
let text = &file.text;
let locals = r
.locals
.iter()
.find(|(p, _)| p.to_string_lossy().ends_with("util.bynk"))
.map(|(_, l)| l.clone())
.expect("util.bynk locals");
let total = text.find("total").expect("total def");
assert_eq!(
describe_local_at(&locals, text, total).as_deref(),
Some("```bynk\nlet total: Int\n```")
);
let xs_param = text.find("xs: List[Int]").expect("xs param");
assert_eq!(
describe_local_at(&locals, text, xs_param).as_deref(),
Some("```bynk\nparam xs: List[Int]\n```")
);
assert!(describe_local_at(&locals, text, text.find("fn").unwrap()).is_none());
}
const HOLE_SRC: &str = "\
commons demo.text
fn shout(s: String) -> String {
s
}
fn greet(name: String) -> String {
\"Hi, \\(shout(name))!\"
}
";
fn analyse_hole_fixture(test_name: &str) -> (String, Vec<LocalBinding>) {
let root = std::env::temp_dir().join(format!(
"bynk-locals-hole-{test_name}-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
let file = root.join("demo/text.bynk");
std::fs::create_dir_all(file.parent().unwrap()).expect("create dirs");
std::fs::write(&file, HOLE_SRC).expect("write fixture");
let root = root.canonicalize().unwrap_or(root);
let r = bynk_ide::diagnose_project(&root, &std::collections::HashMap::new());
let text = r
.files
.iter()
.find(|f| f.source_path.to_string_lossy().ends_with("text.bynk"))
.expect("text.bynk analysed")
.text
.clone();
let locals = r
.locals
.iter()
.find(|(p, _)| p.to_string_lossy().ends_with("text.bynk"))
.map(|(_, l)| l.clone())
.expect("text.bynk locals");
(text, locals)
}
fn nth_offset(text: &str, needle: &str, n: usize) -> usize {
text.match_indices(needle).nth(n).expect("occurrence").0
}
#[test]
fn hover_describes_a_param_inside_a_hole() {
let (text, locals) = analyse_hole_fixture("hover");
let in_hole = nth_offset(&text, "name", 1) + 1; assert_eq!(
describe_local_at(&locals, &text, in_hole).as_deref(),
Some("```bynk\nparam name: String\n```"),
"hover inside the hole renders the param summary"
);
}
#[test]
fn definition_of_a_param_resolves_from_inside_a_hole() {
let (text, locals) = analyse_hole_fixture("def");
let in_hole = nth_offset(&text, "name", 1) + 1;
let def = local_definition_at(&locals, &text, in_hole).expect("resolves to a def");
let decl = nth_offset(&text, "name", 0); assert_eq!(def.start, decl, "def points at the `name` parameter");
}
#[test]
fn references_include_a_param_use_inside_a_hole() {
let (text, locals) = analyse_hole_fixture("refs");
let decl = nth_offset(&text, "name", 0);
let sites = local_sites_at(&locals, &text, decl).expect("on the param");
let in_hole = nth_offset(&text, "name", 1);
assert!(
sites.iter().any(|s| s.start <= in_hole && in_hole < s.end),
"references include the in-hole use at {in_hole}; got {sites:?}"
);
}
}