1use std::collections::{BTreeMap, HashMap};
18use std::path::{Path, PathBuf};
19
20use bynk_check::checker::{Ty, TyId, Types};
21use bynk_check::index::{ProjectIndex, SiteRef, SymbolKey, SymbolKind};
22use bynk_syntax::ast::BaseType;
23use bynk_syntax::span::Span;
24
25pub fn sites_for<'a>(
27 index: &'a ProjectIndex,
28 path: &Path,
29 offset: usize,
30 include_declaration: bool,
31) -> Option<Vec<&'a SiteRef>> {
32 let (key, _) = index.symbol_at(path, offset)?;
33 let mut sites = index.sites(key);
34 if !include_declaration && !sites.is_empty() {
35 sites.remove(0); }
37 Some(sites)
38}
39
40pub fn definition_at<'a>(
43 index: &'a ProjectIndex,
44 path: &Path,
45 offset: usize,
46) -> Option<(&'a SymbolKey, &'a SiteRef)> {
47 let (key, _) = index.symbol_at(path, offset)?;
48 let entry = index.symbols.get(key)?;
49 Some((key, entry.def.as_ref()?))
50}
51
52pub fn prepare_rename<'a>(
56 index: &'a ProjectIndex,
57 path: &Path,
58 offset: usize,
59) -> Option<(&'a SymbolKey, &'a SiteRef)> {
60 index.symbol_at(path, offset)
61}
62
63pub fn workspace_symbols<'a>(
67 index: &'a ProjectIndex,
68 query: &str,
69) -> Vec<(&'a SymbolKey, &'a SiteRef)> {
70 let q = query.to_lowercase();
71 let mut out: Vec<_> = index
72 .symbols
73 .iter()
74 .filter(|(k, _)| q.is_empty() || k.name.to_lowercase().contains(&q))
75 .filter_map(|(k, e)| e.def.as_ref().map(|d| (k, d)))
76 .collect();
77 out.sort_by(|a, b| (&a.0.name, &a.0.unit).cmp(&(&b.0.name, &b.0.unit)));
78 out
79}
80
81pub fn code_lenses<'a>(index: &'a ProjectIndex, path: &Path) -> Vec<(&'a SiteRef, &'a [SiteRef])> {
86 let mut out: Vec<(&SiteRef, &[SiteRef])> = index
87 .symbols
88 .values()
89 .filter_map(|e| {
90 let def = e.def.as_ref()?;
91 (def.path == path).then_some((def, e.refs.as_slice()))
92 })
93 .collect();
94 out.sort_by_key(|(def, _)| (def.span.start, def.span.end));
95 out
96}
97
98pub fn capability_provider_lenses<'a>(
105 index: &'a ProjectIndex,
106 path: &Path,
107) -> Vec<(&'a SiteRef, Vec<&'a SiteRef>)> {
108 let mut out: Vec<(&SiteRef, Vec<&SiteRef>)> = index
109 .symbols
110 .iter()
111 .filter(|(key, _)| key.kind == SymbolKind::Capability)
112 .filter_map(|(key, entry)| {
113 let def = entry.def.as_ref()?;
114 if def.path != path {
115 return None;
116 }
117 let providers = implementations(index, key);
118 (!providers.is_empty()).then_some((def, providers))
119 })
120 .collect();
121 out.sort_by_key(|(def, _)| (def.span.start, def.span.end));
122 out
123}
124
125pub fn refinement_family_lenses<'a>(
133 index: &'a ProjectIndex,
134 path: &Path,
135) -> Vec<(&'a SiteRef, BaseType, Vec<&'a SiteRef>)> {
136 let mut out: Vec<(&SiteRef, BaseType, Vec<&SiteRef>)> = index
137 .symbols
138 .iter()
139 .filter(|(key, _)| key.kind == SymbolKind::Type)
140 .filter_map(|(key, entry)| {
141 let def = entry.def.as_ref()?;
142 if def.path != path {
143 return None;
144 }
145 let base = index.refined_base(key)?;
146 let mut family: Vec<&SiteRef> = index
147 .refinements_over(base)
148 .filter_map(|e| index.symbols.get(&e.ty)?.def.as_ref())
149 .collect();
150 family.sort_by_key(|d| (d.path.clone(), d.span.start, d.span.end));
151 family.dedup();
152 (family.len() >= 2).then_some((def, base, family))
153 })
154 .collect();
155 out.sort_by_key(|(def, _, _)| (def.span.start, def.span.end));
156 out
157}
158
159pub struct CallRelation<'a> {
166 pub key: &'a SymbolKey,
167 pub def: &'a SiteRef,
168 pub sites: Vec<&'a SiteRef>,
169}
170
171pub fn prepare_call_hierarchy<'a>(
175 index: &'a ProjectIndex,
176 path: &Path,
177 offset: usize,
178) -> Option<(&'a SymbolKey, &'a SiteRef)> {
179 definition_at(index, path, offset)
180}
181
182fn group_calls<'a>(
188 index: &'a ProjectIndex,
189 edges: impl Iterator<Item = &'a bynk_check::index::CallEdge>,
190 pick: impl Fn(&'a bynk_check::index::CallEdge) -> &'a SymbolKey,
191) -> Vec<CallRelation<'a>> {
192 let mut by_key: BTreeMap<&SymbolKey, Vec<&SiteRef>> = BTreeMap::new();
193 for edge in edges {
194 by_key.entry(pick(edge)).or_default().push(&edge.site);
195 }
196 let mut out: Vec<CallRelation<'a>> = by_key
197 .into_iter()
198 .filter_map(|(key, mut sites)| {
199 let def = index.symbols.get(key)?.def.as_ref()?;
200 sites.sort();
201 Some(CallRelation { key, def, sites })
202 })
203 .collect();
204 out.sort_by_key(|r| (r.def.path.clone(), r.def.span.start, r.def.span.end));
205 out
206}
207
208pub fn incoming_calls<'a>(index: &'a ProjectIndex, key: &SymbolKey) -> Vec<CallRelation<'a>> {
211 group_calls(index, index.calls_into(key), |e| &e.caller)
212}
213
214pub fn outgoing_calls<'a>(index: &'a ProjectIndex, key: &SymbolKey) -> Vec<CallRelation<'a>> {
217 group_calls(index, index.calls_from(key), |e| &e.callee)
218}
219
220pub fn implementations<'a>(index: &'a ProjectIndex, key: &SymbolKey) -> Vec<&'a SiteRef> {
225 let mut defs: Vec<&SiteRef> = index
226 .impls_of(key)
227 .filter_map(|e| index.symbols.get(&e.provider)?.def.as_ref())
228 .collect();
229 defs.sort_by_key(|d| (d.path.clone(), d.span.start, d.span.end));
230 defs.dedup();
231 defs
232}
233
234pub fn type_definitions_named<'a>(index: &'a ProjectIndex, name: &str) -> Vec<&'a SiteRef> {
240 let mut defs: Vec<&SiteRef> = index
241 .symbols
242 .iter()
243 .filter(|(k, _)| k.kind == SymbolKind::Type && k.name == name)
244 .filter_map(|(_, e)| e.def.as_ref())
245 .collect();
246 defs.sort_by_key(|d| (d.path.clone(), d.span.start, d.span.end));
247 defs.dedup();
248 defs
249}
250
251const DOC_LINK_MEMBER_KINDS: &[SymbolKind] = &[
255 SymbolKind::Field,
256 SymbolKind::Method,
257 SymbolKind::CapabilityOp,
258 SymbolKind::Handler,
259];
260
261const DOC_LINK_BARE_KINDS: &[SymbolKind] = &[
263 SymbolKind::Type,
264 SymbolKind::Fn,
265 SymbolKind::Capability,
266 SymbolKind::Service,
267 SymbolKind::Agent,
268 SymbolKind::Provider,
269 SymbolKind::Actor,
270 SymbolKind::Asset,
271 SymbolKind::Embed,
272];
273
274pub fn resolve_doc_link<'a>(
292 index: &'a ProjectIndex,
293 doc_scope: &HashMap<String, Vec<String>>,
294 owner_unit: &str,
295 name: &str,
296) -> Option<&'a SiteRef> {
297 let kinds: &[SymbolKind] = if name.contains('.') {
298 DOC_LINK_MEMBER_KINDS
299 } else {
300 DOC_LINK_BARE_KINDS
301 };
302 let scope = doc_scope.get(owner_unit)?;
303 for unit in scope {
304 let mut hit: Option<&SiteRef> = None;
305 let mut count = 0usize;
306 for &kind in kinds {
307 let key = SymbolKey {
308 unit: unit.clone(),
309 kind,
310 name: name.to_string(),
311 };
312 if let Some(def) = index.symbols.get(&key).and_then(|e| e.def.as_ref()) {
313 count += 1;
314 hit = Some(def);
315 }
316 }
317 match count {
318 0 => continue,
319 1 => return hit,
320 _ => return None, }
322 }
323 None
324}
325
326pub fn named_type_target(ty: TyId, tys: &Types) -> Option<String> {
332 match &*tys.get(ty) {
333 Ty::Named { name, .. } => Some(name.clone()),
334 Ty::Option(t) | Ty::Effect(t) | Ty::List(t) | Ty::HttpResult(t) => {
335 named_type_target(*t, tys)
336 }
337 _ => None,
338 }
339}
340
341pub fn document_highlights<'a>(
346 index: &'a ProjectIndex,
347 path: &Path,
348 offset: usize,
349) -> Option<Vec<&'a SiteRef>> {
350 let sites = sites_for(index, path, offset, true)?;
351 Some(sites.into_iter().filter(|s| s.path == path).collect())
352}
353
354#[derive(Debug, Clone)]
357pub struct RenamePlan {
358 pub key: SymbolKey,
359 pub new_name: String,
360 pub edits: BTreeMap<PathBuf, Vec<Span>>,
361}
362
363pub fn plan_rename(
366 index: &ProjectIndex,
367 path: &Path,
368 offset: usize,
369 new_name: &str,
370) -> Result<RenamePlan, String> {
371 validate_new_name(new_name)?;
372 let (key, _) = index.symbol_at(path, offset).ok_or_else(|| {
373 "no renameable symbol at the cursor — types, fns, methods, record fields, \
374 capability ops, capabilities, services, agents and providers rename; \
375 local bindings and unit names are not yet supported"
376 .to_string()
377 })?;
378 if key_segment(&key.name) == new_name {
379 return Err(format!("`{new_name}` is already the symbol's name"));
380 }
381 let mut edits: BTreeMap<PathBuf, Vec<Span>> = BTreeMap::new();
382 for site in index.sites(key) {
383 edits.entry(site.path.clone()).or_default().push(site.span);
384 }
385 for spans in edits.values_mut() {
386 spans.sort();
387 spans.dedup();
388 }
389 Ok(RenamePlan {
390 key: key.clone(),
391 new_name: new_name.to_string(),
392 edits,
393 })
394}
395
396pub fn validate_new_name(name: &str) -> Result<(), String> {
399 let err = || format!("`{name}` is not a valid Bynk identifier");
400 let tokens = bynk_syntax::lexer::tokenize(name).map_err(|_| err())?;
401 match tokens.as_slice() {
402 [t] if matches!(t.kind, bynk_syntax::lexer::TokenKind::Ident)
403 && t.span.start == 0
404 && t.span.end == name.len() =>
405 {
406 Ok(())
407 }
408 _ => Err(err()),
409 }
410}
411
412pub fn apply_edits(text: &str, spans: &[Span], new_name: &str) -> String {
414 let mut out = String::with_capacity(text.len());
415 let mut last = 0;
416 for s in spans {
417 out.push_str(&text[last..s.start]);
418 out.push_str(new_name);
419 last = s.end;
420 }
421 out.push_str(&text[last..]);
422 out
423}
424
425pub fn remap_site(site: &SiteRef, plan: &RenamePlan) -> SiteRef {
428 let Some(spans) = plan.edits.get(&site.path) else {
429 return site.clone();
430 };
431 let delta = plan.new_name.len() as isize - key_segment(&plan.key.name).len() as isize;
434 let shift: isize = spans.iter().filter(|s| s.end <= site.span.start).count() as isize * delta;
435 let start = (site.span.start as isize + shift) as usize;
436 let end = if spans.binary_search(&site.span).is_ok() {
437 start + plan.new_name.len()
438 } else {
439 (site.span.end as isize + shift) as usize
440 };
441 SiteRef {
442 path: site.path.clone(),
443 span: Span::new_in(site.span.file, start, end),
444 }
445}
446
447pub fn index_unchanged_modulo_rename(
452 pre: &ProjectIndex,
453 post: &ProjectIndex,
454 plan: &RenamePlan,
455) -> bool {
456 let target = renamed_key_name(&plan.key.name, &plan.new_name);
460 pre.equals_modulo_rename(post, &plan.key, &target, |s| remap_site(s, plan))
461}
462
463fn renamed_key_name(old: &str, new_segment: &str) -> String {
467 match old.rfind('.') {
468 Some(i) => format!("{}.{new_segment}", &old[..i]),
469 None => new_segment.to_string(),
470 }
471}
472
473fn key_segment(name: &str) -> &str {
476 name.rsplit('.').next().unwrap_or(name)
477}
478
479pub fn no_new_diagnostics(
483 pre: &[(PathBuf, String)],
484 post: &[(PathBuf, String)],
485) -> Result<(), String> {
486 let mut budget: HashMap<(&Path, &str), isize> = HashMap::new();
487 for (p, c) in pre {
488 *budget.entry((p.as_path(), c.as_str())).or_default() += 1;
489 }
490 for (p, c) in post {
491 let n = budget.entry((p.as_path(), c.as_str())).or_default();
492 *n -= 1;
493 if *n < 0 {
494 return Err(format!(
495 "rename would introduce `{c}` in {} — refused",
496 p.display()
497 ));
498 }
499 }
500 Ok(())
501}
502
503pub fn semantic_tokens_legend() -> tower_lsp::lsp_types::SemanticTokensLegend {
511 use tower_lsp::lsp_types::{SemanticTokenModifier, SemanticTokenType, SemanticTokensLegend};
512 SemanticTokensLegend {
513 token_types: vec![
514 SemanticTokenType::TYPE,
515 SemanticTokenType::FUNCTION,
516 SemanticTokenType::new("capability"),
517 SemanticTokenType::new("service"),
518 SemanticTokenType::new("agent"),
519 SemanticTokenType::new("provider"),
520 SemanticTokenType::VARIABLE,
523 SemanticTokenType::METHOD,
526 SemanticTokenType::PROPERTY,
529 SemanticTokenType::new("actor"),
532 SemanticTokenType::DECORATOR,
536 SemanticTokenType::new("messages"),
540 SemanticTokenType::COMMENT,
546 SemanticTokenType::new("asset"),
550 SemanticTokenType::new("embed"),
553 ],
554 token_modifiers: vec![
555 SemanticTokenModifier::DECLARATION,
556 SemanticTokenModifier::new("refined"),
557 SemanticTokenModifier::new("opaque"),
558 SemanticTokenModifier::new("platformNative"),
559 SemanticTokenModifier::DOCUMENTATION,
562 ],
563 }
564}
565
566fn token_type_index(kind: SymbolKind) -> u32 {
568 match kind {
569 SymbolKind::Type => 0,
570 SymbolKind::Fn => 1,
571 SymbolKind::Capability => 2,
572 SymbolKind::Service => 3,
573 SymbolKind::Agent => 4,
574 SymbolKind::Provider => 5,
575 SymbolKind::Method => 7,
577 SymbolKind::CapabilityOp => 7,
579 SymbolKind::Field => 8,
580 SymbolKind::Actor => 9,
582 SymbolKind::Handler => 7,
584 SymbolKind::Messages => 11,
586 SymbolKind::Asset => 13,
588 SymbolKind::Embed => 14,
590 }
591}
592
593const TOK_LOCAL: u32 = 6;
595
596const TOK_DECORATOR: u32 = 10;
598
599const MOD_DECLARATION: u32 = 1 << 0;
600const MOD_REFINED: u32 = 1 << 1;
601const MOD_OPAQUE: u32 = 1 << 2;
602const MOD_PLATFORM_NATIVE: u32 = 1 << 3;
603
604fn modifier_bits(m: bynk_check::index::SymbolModifiers) -> u32 {
605 (if m.refined { MOD_REFINED } else { 0 })
606 | (if m.opaque { MOD_OPAQUE } else { 0 })
607 | (if m.platform_native {
608 MOD_PLATFORM_NATIVE
609 } else {
610 0
611 })
612}
613
614pub fn semantic_tokens(
621 index: &ProjectIndex,
622 local_tokens: &[(Span, bool)],
623 decorator_tokens: &[Span],
624 path: &Path,
625 text: &str,
626 range: Option<Span>,
627) -> Vec<tower_lsp::lsp_types::SemanticToken> {
628 let in_scope = |span: Span| {
629 span.end <= text.len() && range.is_none_or(|r| span.end > r.start && span.start < r.end)
630 };
631 let mut raw: Vec<(Span, u32, u32)> = Vec::new();
632 for (key, entry) in &index.symbols {
633 let ty = token_type_index(key.kind);
634 let mods = modifier_bits(entry.modifiers);
635 if let Some(def) = &entry.def
636 && def.path == path
637 && in_scope(def.span)
638 {
639 raw.push((def.span, ty, mods | MOD_DECLARATION));
640 }
641 for site in &entry.refs {
642 if site.path == path && in_scope(site.span) {
643 raw.push((site.span, ty, mods));
644 }
645 }
646 }
647 for fr in &index.foreign_refs {
648 if fr.site.path == path && in_scope(fr.site.span) {
649 raw.push((
650 fr.site.span,
651 token_type_index(fr.kind),
652 modifier_bits(fr.modifiers),
653 ));
654 }
655 }
656 for &(span, is_decl) in local_tokens {
661 if in_scope(span) {
662 raw.push((span, TOK_LOCAL, if is_decl { MOD_DECLARATION } else { 0 }));
663 }
664 }
665 for &span in decorator_tokens {
670 if in_scope(span) {
671 raw.push((span, TOK_DECORATOR, 0));
672 }
673 }
674 raw.sort_by_key(|(span, _, _)| (span.start, span.end));
677 let mut data = Vec::with_capacity(raw.len());
678 let positions = crate::position::PositionMap::new(text);
682 let (mut prev_line, mut prev_start) = (0u32, 0u32);
683 for (span, token_type, token_modifiers_bitset) in raw {
684 let pos = positions.position(span.start);
685 let delta_line = pos.line - prev_line;
686 let delta_start = if delta_line == 0 {
687 pos.character - prev_start
688 } else {
689 pos.character
690 };
691 data.push(tower_lsp::lsp_types::SemanticToken {
692 delta_line,
693 delta_start,
694 length: text[span.range()].encode_utf16().count() as u32,
697 token_type,
698 token_modifiers_bitset,
699 });
700 prev_line = pos.line;
701 prev_start = pos.character;
702 }
703 data
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use bynk_check::index::{SymbolEntry, SymbolKind};
710
711 fn site(path: &str, start: usize, end: usize) -> SiteRef {
712 SiteRef {
713 path: PathBuf::from(path),
714 span: Span::new(start, end),
715 }
716 }
717
718 fn key(unit: &str, kind: SymbolKind, name: &str) -> SymbolKey {
719 SymbolKey {
720 unit: unit.into(),
721 kind,
722 name: name.into(),
723 }
724 }
725
726 fn index_with(entries: Vec<(SymbolKey, SiteRef, Vec<SiteRef>)>) -> ProjectIndex {
727 let mut index = ProjectIndex::default();
728 for (k, def, refs) in entries {
729 index.symbols.insert(
730 k,
731 SymbolEntry {
732 def: Some(def),
733 refs,
734 ..Default::default()
735 },
736 );
737 }
738 index
739 }
740
741 #[test]
742 fn new_name_validation() {
743 assert!(validate_new_name("Money2").is_ok());
744 assert!(validate_new_name("snake_case").is_ok());
745 assert!(validate_new_name("fn").is_err(), "keyword");
746 assert!(validate_new_name("two words").is_err());
747 assert!(validate_new_name("1abc").is_err());
748 assert!(validate_new_name("a.b").is_err());
749 assert!(validate_new_name("").is_err());
750 }
751
752 #[test]
753 fn apply_and_remap_agree() {
754 let text = "fn helper(x: Int) -> Int { helper(x) }";
757 let k = key("demo.a", SymbolKind::Fn, "helper");
758 let index = index_with(vec![(
759 k.clone(),
760 site("a.bynk", 3, 9),
761 vec![site("a.bynk", 27, 33)],
762 )]);
763 let plan = plan_rename(&index, Path::new("a.bynk"), 4, "do_it").unwrap();
764 let edited = apply_edits(text, &plan.edits[Path::new("a.bynk")], "do_it");
765 assert_eq!(edited, "fn do_it(x: Int) -> Int { do_it(x) }");
766
767 for (old, expected) in [
769 (site("a.bynk", 3, 9), "do_it"),
770 (site("a.bynk", 27, 33), "do_it"),
771 ] {
772 let new = remap_site(&old, &plan);
773 assert_eq!(&edited[new.span.range()], expected);
774 }
775 let unrelated = site("a.bynk", 34, 35); let new = remap_site(&unrelated, &plan);
778 assert_eq!(&edited[new.span.range()], "x");
779 }
780
781 #[test]
782 fn references_listing_orders_definition_first() {
783 let k = key("demo.a", SymbolKind::Type, "Money");
784 let index = index_with(vec![(
785 k,
786 site("a.bynk", 5, 10),
787 vec![site("b.bynk", 1, 6), site("a.bynk", 20, 25)],
788 )]);
789 let all = sites_for(&index, Path::new("b.bynk"), 3, true).unwrap();
790 assert_eq!(all.len(), 3);
791 assert_eq!(all[0].path, PathBuf::from("a.bynk"));
792 assert_eq!(all[0].span, Span::new(5, 10));
793 let without_decl = sites_for(&index, Path::new("b.bynk"), 3, false).unwrap();
794 assert_eq!(without_decl.len(), 2);
795 }
796
797 #[test]
798 fn rename_refuses_unindexed_positions_and_same_name() {
799 let k = key("demo.a", SymbolKind::Fn, "helper");
800 let index = index_with(vec![(k, site("a.bynk", 3, 9), vec![])]);
801 assert!(plan_rename(&index, Path::new("a.bynk"), 100, "x").is_err());
802 assert!(plan_rename(&index, Path::new("a.bynk"), 4, "helper").is_err());
803 }
804
805 #[test]
806 fn index_equality_detects_escape() {
807 let helper = key("demo.a", SymbolKind::Fn, "helper");
809 let money = key("demo.a", SymbolKind::Type, "Money");
810 let pre = index_with(vec![
811 (helper.clone(), site("a.bynk", 3, 9), vec![]),
812 (money.clone(), site("a.bynk", 50, 55), vec![]),
813 ]);
814 let plan = plan_rename(&pre, Path::new("a.bynk"), 4, "shadow").unwrap();
815
816 let honest = index_with(vec![
818 (
819 key("demo.a", SymbolKind::Fn, "shadow"),
820 site("a.bynk", 3, 9),
821 vec![],
822 ),
823 (money.clone(), site("a.bynk", 50, 55), vec![]),
824 ]);
825 assert!(index_unchanged_modulo_rename(&pre, &honest, &plan));
826
827 let escape = index_with(vec![
830 (
831 key("demo.a", SymbolKind::Fn, "shadow"),
832 site("a.bynk", 3, 9),
833 vec![site("a.bynk", 70, 76)],
834 ),
835 (money, site("a.bynk", 50, 55), vec![]),
836 ]);
837 assert!(!index_unchanged_modulo_rename(&pre, &escape, &plan));
838 }
839
840 #[test]
841 fn method_rename_edits_the_member_segment_and_remaps_the_compound_key() {
842 let bump = key("demo.a", SymbolKind::Method, "Counter.bump");
847 let other = key("demo.a", SymbolKind::Type, "Counter");
848 let pre = index_with(vec![
849 (other.clone(), site("a.bynk", 0, 5), vec![]),
850 (
852 bump.clone(),
853 site("a.bynk", 11, 15),
854 vec![site("a.bynk", 40, 44)],
855 ),
856 ]);
857
858 let plan = plan_rename(&pre, Path::new("a.bynk"), 12, "increment").unwrap();
860 assert_eq!(plan.key.name, "Counter.bump");
861 assert_eq!(plan.new_name, "increment");
862 assert!(plan_rename(&pre, Path::new("a.bynk"), 12, "bump").is_err());
864
865 let post = index_with(vec![
868 (other, site("a.bynk", 0, 5), vec![]),
869 (
870 key("demo.a", SymbolKind::Method, "Counter.increment"),
871 site("a.bynk", 11, 20),
872 vec![site("a.bynk", 45, 54)],
873 ),
874 ]);
875 assert!(
876 index_unchanged_modulo_rename(&pre, &post, &plan),
877 "compound key remaps to Counter.increment and segment-based delta lines the spans up"
878 );
879 }
880
881 #[test]
882 fn workspace_symbols_filters_and_orders() {
883 let index = index_with(vec![
884 (
885 key("demo.a", SymbolKind::Type, "Money"),
886 site("a.bynk", 5, 10),
887 vec![],
888 ),
889 (
890 key("demo.b", SymbolKind::Fn, "moneyMaker"),
891 site("b.bynk", 3, 13),
892 vec![],
893 ),
894 (
895 key("demo.a", SymbolKind::Fn, "helper"),
896 site("a.bynk", 40, 46),
897 vec![],
898 ),
899 ]);
900 let hits = workspace_symbols(&index, "money");
902 assert_eq!(
903 hits.iter()
904 .map(|(k, _)| k.name.as_str())
905 .collect::<Vec<_>>(),
906 vec!["Money", "moneyMaker"]
907 );
908 assert_eq!(workspace_symbols(&index, "").len(), 3);
910 assert!(workspace_symbols(&index, "nothing").is_empty());
911 }
912
913 #[test]
914 fn document_highlights_are_file_scoped() {
915 let k = key("demo.a", SymbolKind::Type, "Money");
916 let index = index_with(vec![(
917 k,
918 site("a.bynk", 5, 10),
919 vec![site("b.bynk", 1, 6), site("a.bynk", 20, 25)],
920 )]);
921 let highlights = document_highlights(&index, Path::new("a.bynk"), 7).unwrap();
923 assert_eq!(highlights.len(), 2);
924 assert!(highlights.iter().all(|s| s.path == Path::new("a.bynk")));
925 assert!(document_highlights(&index, Path::new("a.bynk"), 100).is_none());
927 }
928
929 #[test]
930 fn diagnostic_budget_allows_removals_refuses_additions() {
931 let pre = vec![
932 (PathBuf::from("a.bynk"), "bynk.x".to_string()),
933 (PathBuf::from("a.bynk"), "bynk.x".to_string()),
934 ];
935 let same = pre.clone();
936 assert!(no_new_diagnostics(&pre, &same).is_ok());
937 assert!(no_new_diagnostics(&pre, &pre[..1]).is_ok());
938 let mut more = pre.clone();
939 more.push((PathBuf::from("b.bynk"), "bynk.resolve.duplicate_fn".into()));
940 assert!(no_new_diagnostics(&pre, &more).is_err());
941 }
942
943 #[test]
949 fn legend_is_frozen() {
950 let legend = semantic_tokens_legend();
951 let types: Vec<&str> = legend.token_types.iter().map(|t| t.as_str()).collect();
952 assert_eq!(
953 types,
954 [
955 "type",
956 "function",
957 "capability",
958 "service",
959 "agent",
960 "provider",
961 "variable", "method", "property", "actor", "decorator", "messages", "comment", "asset", "embed", ]
971 );
972 let modifiers: Vec<&str> = legend.token_modifiers.iter().map(|m| m.as_str()).collect();
973 assert_eq!(
974 modifiers,
975 [
976 "declaration",
977 "refined",
978 "opaque",
979 "platformNative",
980 "documentation"
981 ]
982 );
983 }
984
985 #[test]
989 fn doc_lines_get_no_semantic_token() {
990 let text = "commons m\n\n --- A doc.\n// a comment\ntype T = Int\n";
991 let tokens = semantic_tokens(
992 &ProjectIndex::default(),
993 &[],
994 &[],
995 Path::new("a.bynk"),
996 text,
997 None,
998 );
999 assert!(tokens.is_empty(), "{tokens:?}");
1000 let legend = semantic_tokens_legend();
1001 assert_eq!(legend.token_types[12].as_str(), "comment");
1002 assert_eq!(legend.token_modifiers[4].as_str(), "documentation");
1003 }
1004
1005 #[test]
1006 fn code_lenses_count_references_per_definition_in_the_file() {
1007 let index = index_with(vec![
1008 (
1010 key("u", SymbolKind::Fn, "foo"),
1011 site("a.bynk", 3, 6),
1012 vec![site("a.bynk", 20, 23), site("b.bynk", 4, 7)],
1013 ),
1014 (
1016 key("u", SymbolKind::Type, "Bar"),
1017 site("a.bynk", 40, 43),
1018 vec![],
1019 ),
1020 (
1022 key("u", SymbolKind::Fn, "qux"),
1023 site("b.bynk", 0, 3),
1024 vec![],
1025 ),
1026 ]);
1027 let lenses = code_lenses(&index, Path::new("a.bynk"));
1028 assert_eq!(lenses.len(), 2, "two a.bynk defs get lenses");
1029 assert_eq!((lenses[0].0.span.start, lenses[0].1.len()), (3, 2));
1031 assert_eq!((lenses[1].0.span.start, lenses[1].1.len()), (40, 0));
1032 assert!(code_lenses(&index, Path::new("c.bynk")).is_empty());
1033 }
1034
1035 #[test]
1036 fn call_hierarchy_groups_incoming_and_outgoing_by_symbol() {
1037 use bynk_check::index::CallEdge;
1038 let mut index = index_with(vec![
1042 (key("u", SymbolKind::Fn, "a"), site("f.bynk", 3, 4), vec![]),
1043 (
1044 key("u", SymbolKind::Fn, "b"),
1045 site("f.bynk", 40, 41),
1046 vec![],
1047 ),
1048 (
1049 key("u", SymbolKind::Fn, "c"),
1050 site("f.bynk", 80, 81),
1051 vec![],
1052 ),
1053 ]);
1054 let edge = |caller: &str, cs: usize, ce: usize| CallEdge {
1055 caller: key("u", SymbolKind::Fn, caller),
1056 callee: key("u", SymbolKind::Fn, "c"),
1057 site: site("f.bynk", cs, ce),
1058 };
1059 index.calls = vec![edge("a", 10, 11), edge("a", 20, 21), edge("b", 50, 51)];
1060
1061 let into_c = incoming_calls(&index, &key("u", SymbolKind::Fn, "c"));
1062 assert_eq!(into_c.len(), 2);
1064 assert_eq!(
1065 (into_c[0].key.name.as_str(), into_c[0].sites.len()),
1066 ("a", 2)
1067 );
1068 assert_eq!(
1069 (into_c[1].key.name.as_str(), into_c[1].sites.len()),
1070 ("b", 1)
1071 );
1072
1073 let from_a = outgoing_calls(&index, &key("u", SymbolKind::Fn, "a"));
1074 assert_eq!(from_a.len(), 1);
1075 assert_eq!(
1076 (from_a[0].key.name.as_str(), from_a[0].sites.len()),
1077 ("c", 2)
1078 );
1079
1080 assert!(outgoing_calls(&index, &key("u", SymbolKind::Fn, "c")).is_empty());
1082 assert!(incoming_calls(&index, &key("u", SymbolKind::Fn, "ghost")).is_empty());
1083 }
1084
1085 #[test]
1086 fn implementations_lists_provider_defs_for_a_capability() {
1087 use bynk_check::index::ImplEdge;
1088 let mut index = index_with(vec![
1090 (
1091 key("u", SymbolKind::Capability, "Cap"),
1092 site("a.bynk", 10, 13),
1093 vec![],
1094 ),
1095 (
1096 key("u", SymbolKind::Provider, "P1"),
1097 site("a.bynk", 50, 52),
1098 vec![],
1099 ),
1100 (
1101 key("u", SymbolKind::Provider, "P2"),
1102 site("b.bynk", 5, 7),
1103 vec![],
1104 ),
1105 (
1106 key("u", SymbolKind::Capability, "Other"),
1107 site("a.bynk", 80, 85),
1108 vec![],
1109 ),
1110 ]);
1111 let edge = |provider: &str, file: &str, s: usize, e: usize| ImplEdge {
1112 capability: key("u", SymbolKind::Capability, "Cap"),
1113 provider: key("u", SymbolKind::Provider, provider),
1114 site: site(file, s, e),
1115 };
1116 index.impls = vec![edge("P1", "a.bynk", 30, 33), edge("P2", "b.bynk", 20, 23)];
1117
1118 let impls = implementations(&index, &key("u", SymbolKind::Capability, "Cap"));
1120 assert_eq!(impls.len(), 2);
1121 assert_eq!(
1122 (&impls[0].path, impls[0].span.start),
1123 (&PathBuf::from("a.bynk"), 50)
1124 );
1125 assert_eq!(
1126 (&impls[1].path, impls[1].span.start),
1127 (&PathBuf::from("b.bynk"), 5)
1128 );
1129
1130 assert!(implementations(&index, &key("u", SymbolKind::Capability, "Other")).is_empty());
1132 assert!(implementations(&index, &key("u", SymbolKind::Capability, "Ghost")).is_empty());
1133 }
1134
1135 #[test]
1136 fn capability_provider_lenses_pair_a_capability_with_its_providers() {
1137 use bynk_check::index::ImplEdge;
1138 let mut index = index_with(vec![
1141 (
1142 key("u", SymbolKind::Capability, "Cap"),
1143 site("a.bynk", 10, 13),
1144 vec![],
1145 ),
1146 (
1147 key("u", SymbolKind::Provider, "P1"),
1148 site("a.bynk", 50, 52),
1149 vec![],
1150 ),
1151 (
1152 key("u", SymbolKind::Provider, "P2"),
1153 site("b.bynk", 5, 7),
1154 vec![],
1155 ),
1156 (
1157 key("u", SymbolKind::Capability, "Other"),
1158 site("a.bynk", 80, 85),
1159 vec![],
1160 ),
1161 (
1162 key("u", SymbolKind::Capability, "Elsewhere"),
1163 site("b.bynk", 90, 99),
1164 vec![],
1165 ),
1166 ]);
1167 index.impls = vec![
1168 ImplEdge {
1169 capability: key("u", SymbolKind::Capability, "Cap"),
1170 provider: key("u", SymbolKind::Provider, "P1"),
1171 site: site("a.bynk", 30, 33),
1172 },
1173 ImplEdge {
1174 capability: key("u", SymbolKind::Capability, "Cap"),
1175 provider: key("u", SymbolKind::Provider, "P2"),
1176 site: site("b.bynk", 20, 23),
1177 },
1178 ];
1179
1180 let lenses = capability_provider_lenses(&index, Path::new("a.bynk"));
1181 assert_eq!(lenses.len(), 1);
1183 assert_eq!(lenses[0].0.span.start, 10);
1184 assert_eq!(lenses[0].1.len(), 2, "Cap has two providers");
1185 assert!(capability_provider_lenses(&index, Path::new("c.bynk")).is_empty());
1187 }
1188
1189 #[test]
1190 fn refinement_family_lenses_group_refined_types_by_builtin_base() {
1191 use bynk_check::index::RefineEdge;
1192 let mut index = index_with(vec![
1195 (
1196 key("u", SymbolKind::Type, "Email"),
1197 site("a.bynk", 10, 15),
1198 vec![],
1199 ),
1200 (
1201 key("u", SymbolKind::Type, "UserId"),
1202 site("a.bynk", 40, 46),
1203 vec![],
1204 ),
1205 (
1206 key("u", SymbolKind::Type, "Slug"),
1207 site("b.bynk", 5, 9),
1208 vec![],
1209 ),
1210 (
1211 key("u", SymbolKind::Type, "Age"),
1212 site("a.bynk", 70, 73),
1213 vec![],
1214 ),
1215 (
1216 key("u", SymbolKind::Type, "Order"),
1217 site("a.bynk", 90, 95),
1218 vec![],
1219 ),
1220 ]);
1221 let refine = |name: &str, base: BaseType| RefineEdge {
1222 base,
1223 ty: key("u", SymbolKind::Type, name),
1224 };
1225 index.refinements = vec![
1226 refine("Email", BaseType::String),
1227 refine("UserId", BaseType::String),
1228 refine("Slug", BaseType::String),
1229 refine("Age", BaseType::Int),
1230 ];
1231
1232 let lenses = refinement_family_lenses(&index, Path::new("a.bynk"));
1233 assert_eq!(lenses.len(), 2);
1236 assert_eq!(
1238 (lenses[0].0.span.start, lenses[0].1),
1239 (10, BaseType::String)
1240 );
1241 assert_eq!(lenses[0].2.len(), 3, "the String family spans 3 types");
1242 assert_eq!(lenses[1].0.span.start, 40);
1243 assert!(refinement_family_lenses(&index, Path::new("c.bynk")).is_empty());
1245 }
1246
1247 #[test]
1248 fn type_definitions_named_collects_type_defs_by_bare_name() {
1249 let index = index_with(vec![
1251 (
1252 key("a", SymbolKind::Type, "Order"),
1253 site("a.bynk", 10, 15),
1254 vec![],
1255 ),
1256 (
1257 key("b", SymbolKind::Type, "Order"),
1258 site("b.bynk", 4, 9),
1259 vec![],
1260 ),
1261 (
1262 key("a", SymbolKind::Fn, "Order"),
1263 site("a.bynk", 40, 45),
1264 vec![],
1265 ),
1266 ]);
1267 let defs = type_definitions_named(&index, "Order");
1269 assert_eq!(defs.len(), 2);
1270 assert_eq!(
1271 (&defs[0].path, defs[0].span.start),
1272 (&PathBuf::from("a.bynk"), 10)
1273 );
1274 assert_eq!(
1275 (&defs[1].path, defs[1].span.start),
1276 (&PathBuf::from("b.bynk"), 4)
1277 );
1278 assert!(type_definitions_named(&index, "Nope").is_empty());
1280 }
1281
1282 fn doc_scope_with(entries: Vec<(&str, Vec<&str>)>) -> HashMap<String, Vec<String>> {
1283 entries
1284 .into_iter()
1285 .map(|(unit, scope)| {
1286 (
1287 unit.to_string(),
1288 scope.into_iter().map(String::from).collect(),
1289 )
1290 })
1291 .collect()
1292 }
1293
1294 #[test]
1295 fn resolve_doc_link_finds_a_bare_local_hit() {
1296 let index = index_with(vec![(
1297 key("ratelimit", SymbolKind::Agent, "Limiter"),
1298 site("ratelimit.bynk", 10, 17),
1299 vec![],
1300 )]);
1301 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit"])]);
1302 let def = resolve_doc_link(&index, &scope, "ratelimit", "Limiter").unwrap();
1303 assert_eq!(def.path, PathBuf::from("ratelimit.bynk"));
1304 }
1305
1306 #[test]
1307 fn resolve_doc_link_finds_a_dotted_local_hit() {
1308 let index = index_with(vec![(
1309 key("ratelimit", SymbolKind::Field, "RateView.remaining"),
1310 site("ratelimit.bynk", 20, 29),
1311 vec![],
1312 )]);
1313 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit"])]);
1314 let def = resolve_doc_link(&index, &scope, "ratelimit", "RateView.remaining").unwrap();
1315 assert_eq!(def.path, PathBuf::from("ratelimit.bynk"));
1316 }
1317
1318 #[test]
1319 fn resolve_doc_link_follows_the_uses_chain() {
1320 let index = index_with(vec![(
1324 key("window", SymbolKind::Fn, "decide"),
1325 site("window.bynk", 5, 11),
1326 vec![],
1327 )]);
1328 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit", "window"])]);
1329 let def = resolve_doc_link(&index, &scope, "ratelimit", "decide").unwrap();
1330 assert_eq!(def.path, PathBuf::from("window.bynk"));
1331 }
1332
1333 #[test]
1334 fn resolve_doc_link_finds_an_asset_through_uses() {
1335 let index = index_with(vec![(
1337 key("page", SymbolKind::Asset, "style"),
1338 site("page.bynk", 5, 10),
1339 vec![],
1340 )]);
1341 let scope = doc_scope_with(vec![("board", vec!["board", "page"])]);
1342 let def = resolve_doc_link(&index, &scope, "board", "style").unwrap();
1343 assert_eq!(def.path, PathBuf::from("page.bynk"));
1344 }
1345
1346 #[test]
1347 fn resolve_doc_link_finds_an_embed_through_uses() {
1348 let index = index_with(vec![(
1350 key("page", SymbolKind::Embed, "shell"),
1351 site("page.bynk", 5, 10),
1352 vec![],
1353 )]);
1354 let scope = doc_scope_with(vec![("board", vec!["board", "page"])]);
1355 let def = resolve_doc_link(&index, &scope, "board", "shell").unwrap();
1356 assert_eq!(def.path, PathBuf::from("page.bynk"));
1357 }
1358
1359 #[test]
1360 fn resolve_doc_link_follows_the_consumes_chain_after_uses() {
1361 let index = index_with(vec![(
1362 key("platform", SymbolKind::Capability, "Clock"),
1363 site("platform.bynk", 0, 5),
1364 vec![],
1365 )]);
1366 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit", "window", "platform"])]);
1369 let def = resolve_doc_link(&index, &scope, "ratelimit", "Clock").unwrap();
1370 assert_eq!(def.path, PathBuf::from("platform.bynk"));
1371 }
1372
1373 #[test]
1374 fn resolve_doc_link_is_none_when_nothing_in_scope_matches() {
1375 let index = index_with(vec![(
1376 key("ratelimit", SymbolKind::Agent, "Limiter"),
1377 site("ratelimit.bynk", 10, 17),
1378 vec![],
1379 )]);
1380 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit"])]);
1381 assert!(resolve_doc_link(&index, &scope, "ratelimit", "Nope").is_none());
1382 }
1383
1384 #[test]
1385 fn resolve_doc_link_is_none_for_a_synthetic_unit_never_in_scope() {
1386 let index = ProjectIndex::default();
1391 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit", "bynk"])]);
1392 assert!(resolve_doc_link(&index, &scope, "ratelimit", "Clock.now").is_none());
1393 }
1394
1395 #[test]
1396 fn resolve_doc_link_is_none_when_the_owner_unit_has_no_scope_entry() {
1397 let index = ProjectIndex::default();
1398 let scope = doc_scope_with(vec![]);
1399 assert!(resolve_doc_link(&index, &scope, "ratelimit", "Limiter").is_none());
1400 }
1401
1402 #[test]
1403 fn resolve_doc_link_never_guesses_an_ambiguous_first_matching_unit() {
1404 let index = index_with(vec![
1409 (
1410 key("ratelimit", SymbolKind::Type, "Order"),
1411 site("ratelimit.bynk", 10, 15),
1412 vec![],
1413 ),
1414 (
1415 key("ratelimit", SymbolKind::Fn, "Order"),
1416 site("ratelimit.bynk", 40, 45),
1417 vec![],
1418 ),
1419 (
1420 key("window", SymbolKind::Type, "Order"),
1421 site("window.bynk", 0, 5),
1422 vec![],
1423 ),
1424 ]);
1425 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit", "window"])]);
1426 assert!(resolve_doc_link(&index, &scope, "ratelimit", "Order").is_none());
1427 }
1428
1429 #[test]
1430 fn named_type_target_unwraps_single_param_containers() {
1431 use bynk_check::checker::{NamedKind, Types};
1432 use bynk_syntax::ast::BaseType;
1433 let tys = &Types::new();
1434 let order = || {
1435 tys.intern(Ty::Named {
1436 name: "Order".into(),
1437 kind: NamedKind::Record,
1438 args: Vec::new(),
1439 })
1440 };
1441 let target = |t| named_type_target(t, tys);
1442 assert_eq!(target(order()).as_deref(), Some("Order"));
1443 assert_eq!(
1444 target(tys.intern(Ty::Option(order()))).as_deref(),
1445 Some("Order")
1446 );
1447 let effect = tys.intern(Ty::Effect(order()));
1449 assert_eq!(
1450 target(tys.intern(Ty::List(effect))).as_deref(),
1451 Some("Order")
1452 );
1453 assert_eq!(target(tys.intern(Ty::Base(BaseType::Int))), None);
1455 assert_eq!(target(tys.intern(Ty::Result(order(), order()))), None);
1456 assert_eq!(target(tys.intern(Ty::Unit)), None);
1457 }
1458
1459 #[test]
1460 fn tokens_are_delta_encoded_with_modifier_bitsets() {
1461 let text = "type Age = Int\nfn f(a: Age) -> Age {}\n";
1464 let mut index = index_with(vec![(
1465 key("shop", SymbolKind::Type, "Age"),
1466 site("a.bynk", 5, 8),
1467 vec![site("a.bynk", 23, 26), site("a.bynk", 31, 34)],
1468 )]);
1469 index
1470 .symbols
1471 .get_mut(&key("shop", SymbolKind::Type, "Age"))
1472 .unwrap()
1473 .modifiers = bynk_check::index::SymbolModifiers {
1474 refined: true,
1475 ..Default::default()
1476 };
1477 let tokens = semantic_tokens(&index, &[], &[], Path::new("a.bynk"), text, None);
1478 assert_eq!(tokens.len(), 3);
1479 assert_eq!(
1481 (
1482 tokens[0].delta_line,
1483 tokens[0].delta_start,
1484 tokens[0].length
1485 ),
1486 (0, 5, 3)
1487 );
1488 assert_eq!(tokens[0].token_type, 0);
1489 assert_eq!(tokens[0].token_modifiers_bitset, 0b0011);
1490 assert_eq!(
1492 (
1493 tokens[1].delta_line,
1494 tokens[1].delta_start,
1495 tokens[1].length
1496 ),
1497 (1, 8, 3)
1498 );
1499 assert_eq!(tokens[1].token_modifiers_bitset, 0b0010);
1500 assert_eq!(
1502 (
1503 tokens[2].delta_line,
1504 tokens[2].delta_start,
1505 tokens[2].length
1506 ),
1507 (0, 8, 3)
1508 );
1509 }
1510
1511 #[test]
1512 fn foreign_refs_emit_tokens_and_range_filters() {
1513 let text = "given Kv {\n Kv.get(k)\n}\n";
1514 let mut index = ProjectIndex::default();
1515 index.foreign_refs.push(bynk_check::index::ForeignRef {
1516 site: site("a.bynk", 6, 8),
1517 kind: SymbolKind::Capability,
1518 modifiers: bynk_check::index::SymbolModifiers {
1519 platform_native: true,
1520 ..Default::default()
1521 },
1522 });
1523 index.foreign_refs.push(bynk_check::index::ForeignRef {
1524 site: site("a.bynk", 13, 15),
1525 kind: SymbolKind::Capability,
1526 modifiers: bynk_check::index::SymbolModifiers {
1527 platform_native: true,
1528 ..Default::default()
1529 },
1530 });
1531 let all = semantic_tokens(&index, &[], &[], Path::new("a.bynk"), text, None);
1532 assert_eq!(all.len(), 2);
1533 assert_eq!(all[0].token_type, 2); assert_eq!(all[0].token_modifiers_bitset, 0b1000); let ranged = semantic_tokens(
1537 &index,
1538 &[],
1539 &[],
1540 Path::new("a.bynk"),
1541 text,
1542 Some(Span::new(0, 10)),
1543 );
1544 assert_eq!(ranged.len(), 1);
1545 assert!(semantic_tokens(&index, &[], &[], Path::new("b.bynk"), text, None).is_empty());
1547 assert!(
1548 semantic_tokens(
1549 &ProjectIndex::default(),
1550 &[],
1551 &[],
1552 Path::new("a.bynk"),
1553 text,
1554 None
1555 )
1556 .is_empty()
1557 );
1558 }
1559}