use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use bynk_check::checker::Ty;
use bynk_check::index::{ProjectIndex, SiteRef, SymbolKey, SymbolKind};
use bynk_syntax::span::Span;
pub fn sites_for<'a>(
index: &'a ProjectIndex,
path: &Path,
offset: usize,
include_declaration: bool,
) -> Option<Vec<&'a SiteRef>> {
let (key, _) = index.symbol_at(path, offset)?;
let mut sites = index.sites(key);
if !include_declaration && !sites.is_empty() {
sites.remove(0); }
Some(sites)
}
pub fn definition_at<'a>(
index: &'a ProjectIndex,
path: &Path,
offset: usize,
) -> Option<(&'a SymbolKey, &'a SiteRef)> {
let (key, _) = index.symbol_at(path, offset)?;
let entry = index.symbols.get(key)?;
Some((key, entry.def.as_ref()?))
}
pub fn prepare_rename<'a>(
index: &'a ProjectIndex,
path: &Path,
offset: usize,
) -> Option<(&'a SymbolKey, &'a SiteRef)> {
index.symbol_at(path, offset)
}
pub fn workspace_symbols<'a>(
index: &'a ProjectIndex,
query: &str,
) -> Vec<(&'a SymbolKey, &'a SiteRef)> {
let q = query.to_lowercase();
let mut out: Vec<_> = index
.symbols
.iter()
.filter(|(k, _)| q.is_empty() || k.name.to_lowercase().contains(&q))
.filter_map(|(k, e)| e.def.as_ref().map(|d| (k, d)))
.collect();
out.sort_by(|a, b| (&a.0.name, &a.0.unit).cmp(&(&b.0.name, &b.0.unit)));
out
}
pub fn code_lenses<'a>(index: &'a ProjectIndex, path: &Path) -> Vec<(&'a SiteRef, &'a [SiteRef])> {
let mut out: Vec<(&SiteRef, &[SiteRef])> = index
.symbols
.values()
.filter_map(|e| {
let def = e.def.as_ref()?;
(def.path == path).then_some((def, e.refs.as_slice()))
})
.collect();
out.sort_by_key(|(def, _)| (def.span.start, def.span.end));
out
}
pub struct CallRelation<'a> {
pub key: &'a SymbolKey,
pub def: &'a SiteRef,
pub sites: Vec<&'a SiteRef>,
}
pub fn prepare_call_hierarchy<'a>(
index: &'a ProjectIndex,
path: &Path,
offset: usize,
) -> Option<(&'a SymbolKey, &'a SiteRef)> {
definition_at(index, path, offset)
}
fn group_calls<'a>(
index: &'a ProjectIndex,
edges: impl Iterator<Item = &'a bynk_check::index::CallEdge>,
pick: impl Fn(&'a bynk_check::index::CallEdge) -> &'a SymbolKey,
) -> Vec<CallRelation<'a>> {
let mut by_key: BTreeMap<&SymbolKey, Vec<&SiteRef>> = BTreeMap::new();
for edge in edges {
by_key.entry(pick(edge)).or_default().push(&edge.site);
}
let mut out: Vec<CallRelation<'a>> = by_key
.into_iter()
.filter_map(|(key, mut sites)| {
let def = index.symbols.get(key)?.def.as_ref()?;
sites.sort();
Some(CallRelation { key, def, sites })
})
.collect();
out.sort_by_key(|r| (r.def.path.clone(), r.def.span.start, r.def.span.end));
out
}
pub fn incoming_calls<'a>(index: &'a ProjectIndex, key: &SymbolKey) -> Vec<CallRelation<'a>> {
group_calls(index, index.calls_into(key), |e| &e.caller)
}
pub fn outgoing_calls<'a>(index: &'a ProjectIndex, key: &SymbolKey) -> Vec<CallRelation<'a>> {
group_calls(index, index.calls_from(key), |e| &e.callee)
}
pub fn implementations<'a>(index: &'a ProjectIndex, key: &SymbolKey) -> Vec<&'a SiteRef> {
let mut defs: Vec<&SiteRef> = index
.impls_of(key)
.filter_map(|e| index.symbols.get(&e.provider)?.def.as_ref())
.collect();
defs.sort_by_key(|d| (d.path.clone(), d.span.start, d.span.end));
defs.dedup();
defs
}
pub fn type_definitions_named<'a>(index: &'a ProjectIndex, name: &str) -> Vec<&'a SiteRef> {
let mut defs: Vec<&SiteRef> = index
.symbols
.iter()
.filter(|(k, _)| k.kind == SymbolKind::Type && k.name == name)
.filter_map(|(_, e)| e.def.as_ref())
.collect();
defs.sort_by_key(|d| (d.path.clone(), d.span.start, d.span.end));
defs.dedup();
defs
}
pub fn named_type_target(ty: &Ty) -> Option<&str> {
match ty {
Ty::Named { name, .. } => Some(name),
Ty::Option(t) | Ty::Effect(t) | Ty::List(t) | Ty::HttpResult(t) => named_type_target(t),
_ => None,
}
}
pub fn document_highlights<'a>(
index: &'a ProjectIndex,
path: &Path,
offset: usize,
) -> Option<Vec<&'a SiteRef>> {
let sites = sites_for(index, path, offset, true)?;
Some(sites.into_iter().filter(|s| s.path == path).collect())
}
#[derive(Debug, Clone)]
pub struct RenamePlan {
pub key: SymbolKey,
pub new_name: String,
pub edits: BTreeMap<PathBuf, Vec<Span>>,
}
pub fn plan_rename(
index: &ProjectIndex,
path: &Path,
offset: usize,
new_name: &str,
) -> Result<RenamePlan, String> {
validate_new_name(new_name)?;
let (key, _) = index.symbol_at(path, offset).ok_or_else(|| {
"no renameable symbol at the cursor — types, fns, methods, record fields, \
capability ops, capabilities, services, agents and providers rename; \
local bindings and unit names are not yet supported"
.to_string()
})?;
if key_segment(&key.name) == new_name {
return Err(format!("`{new_name}` is already the symbol's name"));
}
let mut edits: BTreeMap<PathBuf, Vec<Span>> = BTreeMap::new();
for site in index.sites(key) {
edits.entry(site.path.clone()).or_default().push(site.span);
}
for spans in edits.values_mut() {
spans.sort();
spans.dedup();
}
Ok(RenamePlan {
key: key.clone(),
new_name: new_name.to_string(),
edits,
})
}
pub fn validate_new_name(name: &str) -> Result<(), String> {
let err = || format!("`{name}` is not a valid Bynk identifier");
let tokens = bynk_syntax::lexer::tokenize(name).map_err(|_| err())?;
match tokens.as_slice() {
[t] if matches!(t.kind, bynk_syntax::lexer::TokenKind::Ident)
&& t.span.start == 0
&& t.span.end == name.len() =>
{
Ok(())
}
_ => Err(err()),
}
}
pub fn apply_edits(text: &str, spans: &[Span], new_name: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut last = 0;
for s in spans {
out.push_str(&text[last..s.start]);
out.push_str(new_name);
last = s.end;
}
out.push_str(&text[last..]);
out
}
pub fn remap_site(site: &SiteRef, plan: &RenamePlan) -> SiteRef {
let Some(spans) = plan.edits.get(&site.path) else {
return site.clone();
};
let delta = plan.new_name.len() as isize - key_segment(&plan.key.name).len() as isize;
let shift: isize = spans.iter().filter(|s| s.end <= site.span.start).count() as isize * delta;
let start = (site.span.start as isize + shift) as usize;
let end = if spans.binary_search(&site.span).is_ok() {
start + plan.new_name.len()
} else {
(site.span.end as isize + shift) as usize
};
SiteRef {
path: site.path.clone(),
span: Span::new(start, end),
}
}
pub fn index_unchanged_modulo_rename(
pre: &ProjectIndex,
post: &ProjectIndex,
plan: &RenamePlan,
) -> bool {
let target = renamed_key_name(&plan.key.name, &plan.new_name);
pre.equals_modulo_rename(post, &plan.key, &target, |s| remap_site(s, plan))
}
fn renamed_key_name(old: &str, new_segment: &str) -> String {
match old.rfind('.') {
Some(i) => format!("{}.{new_segment}", &old[..i]),
None => new_segment.to_string(),
}
}
fn key_segment(name: &str) -> &str {
name.rsplit('.').next().unwrap_or(name)
}
pub fn no_new_diagnostics(
pre: &[(PathBuf, String)],
post: &[(PathBuf, String)],
) -> Result<(), String> {
let mut budget: HashMap<(&Path, &str), isize> = HashMap::new();
for (p, c) in pre {
*budget.entry((p.as_path(), c.as_str())).or_default() += 1;
}
for (p, c) in post {
let n = budget.entry((p.as_path(), c.as_str())).or_default();
*n -= 1;
if *n < 0 {
return Err(format!(
"rename would introduce `{c}` in {} — refused",
p.display()
));
}
}
Ok(())
}
pub fn semantic_tokens_legend() -> tower_lsp::lsp_types::SemanticTokensLegend {
use tower_lsp::lsp_types::{SemanticTokenModifier, SemanticTokenType, SemanticTokensLegend};
SemanticTokensLegend {
token_types: vec![
SemanticTokenType::TYPE,
SemanticTokenType::FUNCTION,
SemanticTokenType::new("capability"),
SemanticTokenType::new("service"),
SemanticTokenType::new("agent"),
SemanticTokenType::new("provider"),
SemanticTokenType::VARIABLE,
SemanticTokenType::METHOD,
SemanticTokenType::PROPERTY,
SemanticTokenType::new("actor"),
],
token_modifiers: vec![
SemanticTokenModifier::DECLARATION,
SemanticTokenModifier::new("refined"),
SemanticTokenModifier::new("opaque"),
SemanticTokenModifier::new("platformNative"),
],
}
}
fn token_type_index(kind: SymbolKind) -> u32 {
match kind {
SymbolKind::Type => 0,
SymbolKind::Fn => 1,
SymbolKind::Capability => 2,
SymbolKind::Service => 3,
SymbolKind::Agent => 4,
SymbolKind::Provider => 5,
SymbolKind::Method => 7,
SymbolKind::CapabilityOp => 7,
SymbolKind::Field => 8,
SymbolKind::Actor => 9,
}
}
const TOK_LOCAL: u32 = 6;
const MOD_DECLARATION: u32 = 1 << 0;
const MOD_REFINED: u32 = 1 << 1;
const MOD_OPAQUE: u32 = 1 << 2;
const MOD_PLATFORM_NATIVE: u32 = 1 << 3;
fn modifier_bits(m: bynk_check::index::SymbolModifiers) -> u32 {
(if m.refined { MOD_REFINED } else { 0 })
| (if m.opaque { MOD_OPAQUE } else { 0 })
| (if m.platform_native {
MOD_PLATFORM_NATIVE
} else {
0
})
}
pub fn semantic_tokens(
index: &ProjectIndex,
local_tokens: &[(Span, bool)],
path: &Path,
text: &str,
range: Option<Span>,
) -> Vec<tower_lsp::lsp_types::SemanticToken> {
let in_scope = |span: Span| {
span.end <= text.len() && range.is_none_or(|r| span.end > r.start && span.start < r.end)
};
let mut raw: Vec<(Span, u32, u32)> = Vec::new();
for (key, entry) in &index.symbols {
let ty = token_type_index(key.kind);
let mods = modifier_bits(entry.modifiers);
if let Some(def) = &entry.def
&& def.path == path
&& in_scope(def.span)
{
raw.push((def.span, ty, mods | MOD_DECLARATION));
}
for site in &entry.refs {
if site.path == path && in_scope(site.span) {
raw.push((site.span, ty, mods));
}
}
}
for fr in &index.foreign_refs {
if fr.site.path == path && in_scope(fr.site.span) {
raw.push((
fr.site.span,
token_type_index(fr.kind),
modifier_bits(fr.modifiers),
));
}
}
for &(span, is_decl) in local_tokens {
if in_scope(span) {
raw.push((span, TOK_LOCAL, if is_decl { MOD_DECLARATION } else { 0 }));
}
}
raw.sort_by_key(|(span, _, _)| (span.start, span.end));
let mut data = Vec::with_capacity(raw.len());
let (mut prev_line, mut prev_start) = (0u32, 0u32);
for (span, token_type, token_modifiers_bitset) in raw {
let pos = crate::position::offset_to_position(text, span.start);
let delta_line = pos.line - prev_line;
let delta_start = if delta_line == 0 {
pos.character - prev_start
} else {
pos.character
};
data.push(tower_lsp::lsp_types::SemanticToken {
delta_line,
delta_start,
length: text[span.range()].encode_utf16().count() as u32,
token_type,
token_modifiers_bitset,
});
prev_line = pos.line;
prev_start = pos.character;
}
data
}
#[cfg(test)]
mod tests {
use super::*;
use bynk_check::index::{SymbolEntry, SymbolKind};
fn site(path: &str, start: usize, end: usize) -> SiteRef {
SiteRef {
path: PathBuf::from(path),
span: Span::new(start, end),
}
}
fn key(unit: &str, kind: SymbolKind, name: &str) -> SymbolKey {
SymbolKey {
unit: unit.into(),
kind,
name: name.into(),
}
}
fn index_with(entries: Vec<(SymbolKey, SiteRef, Vec<SiteRef>)>) -> ProjectIndex {
let mut index = ProjectIndex::default();
for (k, def, refs) in entries {
index.symbols.insert(
k,
SymbolEntry {
def: Some(def),
refs,
..Default::default()
},
);
}
index
}
#[test]
fn new_name_validation() {
assert!(validate_new_name("Money2").is_ok());
assert!(validate_new_name("snake_case").is_ok());
assert!(validate_new_name("fn").is_err(), "keyword");
assert!(validate_new_name("two words").is_err());
assert!(validate_new_name("1abc").is_err());
assert!(validate_new_name("a.b").is_err());
assert!(validate_new_name("").is_err());
}
#[test]
fn apply_and_remap_agree() {
let text = "fn helper(x: Int) -> Int { helper(x) }";
let k = key("demo.a", SymbolKind::Fn, "helper");
let index = index_with(vec![(
k.clone(),
site("a.bynk", 3, 9),
vec![site("a.bynk", 27, 33)],
)]);
let plan = plan_rename(&index, Path::new("a.bynk"), 4, "do_it").unwrap();
let edited = apply_edits(text, &plan.edits[Path::new("a.bynk")], "do_it");
assert_eq!(edited, "fn do_it(x: Int) -> Int { do_it(x) }");
for (old, expected) in [
(site("a.bynk", 3, 9), "do_it"),
(site("a.bynk", 27, 33), "do_it"),
] {
let new = remap_site(&old, &plan);
assert_eq!(&edited[new.span.range()], expected);
}
let unrelated = site("a.bynk", 34, 35); let new = remap_site(&unrelated, &plan);
assert_eq!(&edited[new.span.range()], "x");
}
#[test]
fn references_listing_orders_definition_first() {
let k = key("demo.a", SymbolKind::Type, "Money");
let index = index_with(vec![(
k,
site("a.bynk", 5, 10),
vec![site("b.bynk", 1, 6), site("a.bynk", 20, 25)],
)]);
let all = sites_for(&index, Path::new("b.bynk"), 3, true).unwrap();
assert_eq!(all.len(), 3);
assert_eq!(all[0].path, PathBuf::from("a.bynk"));
assert_eq!(all[0].span, Span::new(5, 10));
let without_decl = sites_for(&index, Path::new("b.bynk"), 3, false).unwrap();
assert_eq!(without_decl.len(), 2);
}
#[test]
fn rename_refuses_unindexed_positions_and_same_name() {
let k = key("demo.a", SymbolKind::Fn, "helper");
let index = index_with(vec![(k, site("a.bynk", 3, 9), vec![])]);
assert!(plan_rename(&index, Path::new("a.bynk"), 100, "x").is_err());
assert!(plan_rename(&index, Path::new("a.bynk"), 4, "helper").is_err());
}
#[test]
fn index_equality_detects_escape() {
let helper = key("demo.a", SymbolKind::Fn, "helper");
let money = key("demo.a", SymbolKind::Type, "Money");
let pre = index_with(vec![
(helper.clone(), site("a.bynk", 3, 9), vec![]),
(money.clone(), site("a.bynk", 50, 55), vec![]),
]);
let plan = plan_rename(&pre, Path::new("a.bynk"), 4, "shadow").unwrap();
let honest = index_with(vec![
(
key("demo.a", SymbolKind::Fn, "shadow"),
site("a.bynk", 3, 9),
vec![],
),
(money.clone(), site("a.bynk", 50, 55), vec![]),
]);
assert!(index_unchanged_modulo_rename(&pre, &honest, &plan));
let escape = index_with(vec![
(
key("demo.a", SymbolKind::Fn, "shadow"),
site("a.bynk", 3, 9),
vec![site("a.bynk", 70, 76)],
),
(money, site("a.bynk", 50, 55), vec![]),
]);
assert!(!index_unchanged_modulo_rename(&pre, &escape, &plan));
}
#[test]
fn method_rename_edits_the_member_segment_and_remaps_the_compound_key() {
let bump = key("demo.a", SymbolKind::Method, "Counter.bump");
let other = key("demo.a", SymbolKind::Type, "Counter");
let pre = index_with(vec![
(other.clone(), site("a.bynk", 0, 5), vec![]),
(
bump.clone(),
site("a.bynk", 11, 15),
vec![site("a.bynk", 40, 44)],
),
]);
let plan = plan_rename(&pre, Path::new("a.bynk"), 12, "increment").unwrap();
assert_eq!(plan.key.name, "Counter.bump");
assert_eq!(plan.new_name, "increment");
assert!(plan_rename(&pre, Path::new("a.bynk"), 12, "bump").is_err());
let post = index_with(vec![
(other, site("a.bynk", 0, 5), vec![]),
(
key("demo.a", SymbolKind::Method, "Counter.increment"),
site("a.bynk", 11, 20),
vec![site("a.bynk", 45, 54)],
),
]);
assert!(
index_unchanged_modulo_rename(&pre, &post, &plan),
"compound key remaps to Counter.increment and segment-based delta lines the spans up"
);
}
#[test]
fn workspace_symbols_filters_and_orders() {
let index = index_with(vec![
(
key("demo.a", SymbolKind::Type, "Money"),
site("a.bynk", 5, 10),
vec![],
),
(
key("demo.b", SymbolKind::Fn, "moneyMaker"),
site("b.bynk", 3, 13),
vec![],
),
(
key("demo.a", SymbolKind::Fn, "helper"),
site("a.bynk", 40, 46),
vec![],
),
]);
let hits = workspace_symbols(&index, "money");
assert_eq!(
hits.iter()
.map(|(k, _)| k.name.as_str())
.collect::<Vec<_>>(),
vec!["Money", "moneyMaker"]
);
assert_eq!(workspace_symbols(&index, "").len(), 3);
assert!(workspace_symbols(&index, "nothing").is_empty());
}
#[test]
fn document_highlights_are_file_scoped() {
let k = key("demo.a", SymbolKind::Type, "Money");
let index = index_with(vec![(
k,
site("a.bynk", 5, 10),
vec![site("b.bynk", 1, 6), site("a.bynk", 20, 25)],
)]);
let highlights = document_highlights(&index, Path::new("a.bynk"), 7).unwrap();
assert_eq!(highlights.len(), 2);
assert!(highlights.iter().all(|s| s.path == Path::new("a.bynk")));
assert!(document_highlights(&index, Path::new("a.bynk"), 100).is_none());
}
#[test]
fn diagnostic_budget_allows_removals_refuses_additions() {
let pre = vec![
(PathBuf::from("a.bynk"), "bynk.x".to_string()),
(PathBuf::from("a.bynk"), "bynk.x".to_string()),
];
let same = pre.clone();
assert!(no_new_diagnostics(&pre, &same).is_ok());
assert!(no_new_diagnostics(&pre, &pre[..1]).is_ok());
let mut more = pre.clone();
more.push((PathBuf::from("b.bynk"), "bynk.resolve.duplicate_fn".into()));
assert!(no_new_diagnostics(&pre, &more).is_err());
}
#[test]
fn legend_is_frozen() {
let legend = semantic_tokens_legend();
let types: Vec<&str> = legend.token_types.iter().map(|t| t.as_str()).collect();
assert_eq!(
types,
[
"type",
"function",
"capability",
"service",
"agent",
"provider",
"variable", "method", "property", "actor", ]
);
let modifiers: Vec<&str> = legend.token_modifiers.iter().map(|m| m.as_str()).collect();
assert_eq!(
modifiers,
["declaration", "refined", "opaque", "platformNative"]
);
}
#[test]
fn code_lenses_count_references_per_definition_in_the_file() {
let index = index_with(vec![
(
key("u", SymbolKind::Fn, "foo"),
site("a.bynk", 3, 6),
vec![site("a.bynk", 20, 23), site("b.bynk", 4, 7)],
),
(
key("u", SymbolKind::Type, "Bar"),
site("a.bynk", 40, 43),
vec![],
),
(
key("u", SymbolKind::Fn, "qux"),
site("b.bynk", 0, 3),
vec![],
),
]);
let lenses = code_lenses(&index, Path::new("a.bynk"));
assert_eq!(lenses.len(), 2, "two a.bynk defs get lenses");
assert_eq!((lenses[0].0.span.start, lenses[0].1.len()), (3, 2));
assert_eq!((lenses[1].0.span.start, lenses[1].1.len()), (40, 0));
assert!(code_lenses(&index, Path::new("c.bynk")).is_empty());
}
#[test]
fn call_hierarchy_groups_incoming_and_outgoing_by_symbol() {
use bynk_check::index::CallEdge;
let mut index = index_with(vec![
(key("u", SymbolKind::Fn, "a"), site("f.bynk", 3, 4), vec![]),
(
key("u", SymbolKind::Fn, "b"),
site("f.bynk", 40, 41),
vec![],
),
(
key("u", SymbolKind::Fn, "c"),
site("f.bynk", 80, 81),
vec![],
),
]);
let edge = |caller: &str, cs: usize, ce: usize| CallEdge {
caller: key("u", SymbolKind::Fn, caller),
callee: key("u", SymbolKind::Fn, "c"),
site: site("f.bynk", cs, ce),
};
index.calls = vec![edge("a", 10, 11), edge("a", 20, 21), edge("b", 50, 51)];
let into_c = incoming_calls(&index, &key("u", SymbolKind::Fn, "c"));
assert_eq!(into_c.len(), 2);
assert_eq!(
(into_c[0].key.name.as_str(), into_c[0].sites.len()),
("a", 2)
);
assert_eq!(
(into_c[1].key.name.as_str(), into_c[1].sites.len()),
("b", 1)
);
let from_a = outgoing_calls(&index, &key("u", SymbolKind::Fn, "a"));
assert_eq!(from_a.len(), 1);
assert_eq!(
(from_a[0].key.name.as_str(), from_a[0].sites.len()),
("c", 2)
);
assert!(outgoing_calls(&index, &key("u", SymbolKind::Fn, "c")).is_empty());
assert!(incoming_calls(&index, &key("u", SymbolKind::Fn, "ghost")).is_empty());
}
#[test]
fn implementations_lists_provider_defs_for_a_capability() {
use bynk_check::index::ImplEdge;
let mut index = index_with(vec![
(
key("u", SymbolKind::Capability, "Cap"),
site("a.bynk", 10, 13),
vec![],
),
(
key("u", SymbolKind::Provider, "P1"),
site("a.bynk", 50, 52),
vec![],
),
(
key("u", SymbolKind::Provider, "P2"),
site("b.bynk", 5, 7),
vec![],
),
(
key("u", SymbolKind::Capability, "Other"),
site("a.bynk", 80, 85),
vec![],
),
]);
let edge = |provider: &str, file: &str, s: usize, e: usize| ImplEdge {
capability: key("u", SymbolKind::Capability, "Cap"),
provider: key("u", SymbolKind::Provider, provider),
site: site(file, s, e),
};
index.impls = vec![edge("P1", "a.bynk", 30, 33), edge("P2", "b.bynk", 20, 23)];
let impls = implementations(&index, &key("u", SymbolKind::Capability, "Cap"));
assert_eq!(impls.len(), 2);
assert_eq!(
(&impls[0].path, impls[0].span.start),
(&PathBuf::from("a.bynk"), 50)
);
assert_eq!(
(&impls[1].path, impls[1].span.start),
(&PathBuf::from("b.bynk"), 5)
);
assert!(implementations(&index, &key("u", SymbolKind::Capability, "Other")).is_empty());
assert!(implementations(&index, &key("u", SymbolKind::Capability, "Ghost")).is_empty());
}
#[test]
fn type_definitions_named_collects_type_defs_by_bare_name() {
let index = index_with(vec![
(
key("a", SymbolKind::Type, "Order"),
site("a.bynk", 10, 15),
vec![],
),
(
key("b", SymbolKind::Type, "Order"),
site("b.bynk", 4, 9),
vec![],
),
(
key("a", SymbolKind::Fn, "Order"),
site("a.bynk", 40, 45),
vec![],
),
]);
let defs = type_definitions_named(&index, "Order");
assert_eq!(defs.len(), 2);
assert_eq!(
(&defs[0].path, defs[0].span.start),
(&PathBuf::from("a.bynk"), 10)
);
assert_eq!(
(&defs[1].path, defs[1].span.start),
(&PathBuf::from("b.bynk"), 4)
);
assert!(type_definitions_named(&index, "Nope").is_empty());
}
#[test]
fn named_type_target_unwraps_single_param_containers() {
use bynk_check::checker::NamedKind;
use bynk_syntax::ast::BaseType;
let order = || Ty::Named {
name: "Order".into(),
kind: NamedKind::Record,
};
assert_eq!(named_type_target(&order()), Some("Order"));
assert_eq!(
named_type_target(&Ty::Option(Box::new(order()))),
Some("Order")
);
assert_eq!(
named_type_target(&Ty::List(Box::new(Ty::Effect(Box::new(order()))))),
Some("Order")
);
assert_eq!(named_type_target(&Ty::Base(BaseType::Int)), None);
assert_eq!(
named_type_target(&Ty::Result(Box::new(order()), Box::new(order()))),
None
);
assert_eq!(named_type_target(&Ty::Unit), None);
}
#[test]
fn tokens_are_delta_encoded_with_modifier_bitsets() {
let text = "type Age = Int\nfn f(a: Age) -> Age {}\n";
let mut index = index_with(vec![(
key("shop", SymbolKind::Type, "Age"),
site("a.bynk", 5, 8),
vec![site("a.bynk", 23, 26), site("a.bynk", 31, 34)],
)]);
index
.symbols
.get_mut(&key("shop", SymbolKind::Type, "Age"))
.unwrap()
.modifiers = bynk_check::index::SymbolModifiers {
refined: true,
..Default::default()
};
let tokens = semantic_tokens(&index, &[], Path::new("a.bynk"), text, None);
assert_eq!(tokens.len(), 3);
assert_eq!(
(
tokens[0].delta_line,
tokens[0].delta_start,
tokens[0].length
),
(0, 5, 3)
);
assert_eq!(tokens[0].token_type, 0);
assert_eq!(tokens[0].token_modifiers_bitset, 0b0011);
assert_eq!(
(
tokens[1].delta_line,
tokens[1].delta_start,
tokens[1].length
),
(1, 8, 3)
);
assert_eq!(tokens[1].token_modifiers_bitset, 0b0010);
assert_eq!(
(
tokens[2].delta_line,
tokens[2].delta_start,
tokens[2].length
),
(0, 8, 3)
);
}
#[test]
fn foreign_refs_emit_tokens_and_range_filters() {
let text = "given Kv {\n Kv.get(k)\n}\n";
let mut index = ProjectIndex::default();
index.foreign_refs.push(bynk_check::index::ForeignRef {
site: site("a.bynk", 6, 8),
kind: SymbolKind::Capability,
modifiers: bynk_check::index::SymbolModifiers {
platform_native: true,
..Default::default()
},
});
index.foreign_refs.push(bynk_check::index::ForeignRef {
site: site("a.bynk", 13, 15),
kind: SymbolKind::Capability,
modifiers: bynk_check::index::SymbolModifiers {
platform_native: true,
..Default::default()
},
});
let all = semantic_tokens(&index, &[], Path::new("a.bynk"), text, None);
assert_eq!(all.len(), 2);
assert_eq!(all[0].token_type, 2); assert_eq!(all[0].token_modifiers_bitset, 0b1000); let ranged = semantic_tokens(
&index,
&[],
Path::new("a.bynk"),
text,
Some(Span::new(0, 10)),
);
assert_eq!(ranged.len(), 1);
assert!(semantic_tokens(&index, &[], Path::new("b.bynk"), text, None).is_empty());
assert!(
semantic_tokens(
&ProjectIndex::default(),
&[],
Path::new("a.bynk"),
text,
None
)
.is_empty()
);
}
}