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];
271
272pub fn resolve_doc_link<'a>(
290 index: &'a ProjectIndex,
291 doc_scope: &HashMap<String, Vec<String>>,
292 owner_unit: &str,
293 name: &str,
294) -> Option<&'a SiteRef> {
295 let kinds: &[SymbolKind] = if name.contains('.') {
296 DOC_LINK_MEMBER_KINDS
297 } else {
298 DOC_LINK_BARE_KINDS
299 };
300 let scope = doc_scope.get(owner_unit)?;
301 for unit in scope {
302 let mut hit: Option<&SiteRef> = None;
303 let mut count = 0usize;
304 for &kind in kinds {
305 let key = SymbolKey {
306 unit: unit.clone(),
307 kind,
308 name: name.to_string(),
309 };
310 if let Some(def) = index.symbols.get(&key).and_then(|e| e.def.as_ref()) {
311 count += 1;
312 hit = Some(def);
313 }
314 }
315 match count {
316 0 => continue,
317 1 => return hit,
318 _ => return None, }
320 }
321 None
322}
323
324pub fn named_type_target(ty: TyId, tys: &Types) -> Option<String> {
330 match &*tys.get(ty) {
331 Ty::Named { name, .. } => Some(name.clone()),
332 Ty::Option(t) | Ty::Effect(t) | Ty::List(t) | Ty::HttpResult(t) => {
333 named_type_target(*t, tys)
334 }
335 _ => None,
336 }
337}
338
339pub fn document_highlights<'a>(
344 index: &'a ProjectIndex,
345 path: &Path,
346 offset: usize,
347) -> Option<Vec<&'a SiteRef>> {
348 let sites = sites_for(index, path, offset, true)?;
349 Some(sites.into_iter().filter(|s| s.path == path).collect())
350}
351
352#[derive(Debug, Clone)]
355pub struct RenamePlan {
356 pub key: SymbolKey,
357 pub new_name: String,
358 pub edits: BTreeMap<PathBuf, Vec<Span>>,
359}
360
361pub fn plan_rename(
364 index: &ProjectIndex,
365 path: &Path,
366 offset: usize,
367 new_name: &str,
368) -> Result<RenamePlan, String> {
369 validate_new_name(new_name)?;
370 let (key, _) = index.symbol_at(path, offset).ok_or_else(|| {
371 "no renameable symbol at the cursor — types, fns, methods, record fields, \
372 capability ops, capabilities, services, agents and providers rename; \
373 local bindings and unit names are not yet supported"
374 .to_string()
375 })?;
376 if key_segment(&key.name) == new_name {
377 return Err(format!("`{new_name}` is already the symbol's name"));
378 }
379 let mut edits: BTreeMap<PathBuf, Vec<Span>> = BTreeMap::new();
380 for site in index.sites(key) {
381 edits.entry(site.path.clone()).or_default().push(site.span);
382 }
383 for spans in edits.values_mut() {
384 spans.sort();
385 spans.dedup();
386 }
387 Ok(RenamePlan {
388 key: key.clone(),
389 new_name: new_name.to_string(),
390 edits,
391 })
392}
393
394pub fn validate_new_name(name: &str) -> Result<(), String> {
397 let err = || format!("`{name}` is not a valid Bynk identifier");
398 let tokens = bynk_syntax::lexer::tokenize(name).map_err(|_| err())?;
399 match tokens.as_slice() {
400 [t] if matches!(t.kind, bynk_syntax::lexer::TokenKind::Ident)
401 && t.span.start == 0
402 && t.span.end == name.len() =>
403 {
404 Ok(())
405 }
406 _ => Err(err()),
407 }
408}
409
410pub fn apply_edits(text: &str, spans: &[Span], new_name: &str) -> String {
412 let mut out = String::with_capacity(text.len());
413 let mut last = 0;
414 for s in spans {
415 out.push_str(&text[last..s.start]);
416 out.push_str(new_name);
417 last = s.end;
418 }
419 out.push_str(&text[last..]);
420 out
421}
422
423pub fn remap_site(site: &SiteRef, plan: &RenamePlan) -> SiteRef {
426 let Some(spans) = plan.edits.get(&site.path) else {
427 return site.clone();
428 };
429 let delta = plan.new_name.len() as isize - key_segment(&plan.key.name).len() as isize;
432 let shift: isize = spans.iter().filter(|s| s.end <= site.span.start).count() as isize * delta;
433 let start = (site.span.start as isize + shift) as usize;
434 let end = if spans.binary_search(&site.span).is_ok() {
435 start + plan.new_name.len()
436 } else {
437 (site.span.end as isize + shift) as usize
438 };
439 SiteRef {
440 path: site.path.clone(),
441 span: Span::new_in(site.span.file, start, end),
442 }
443}
444
445pub fn index_unchanged_modulo_rename(
450 pre: &ProjectIndex,
451 post: &ProjectIndex,
452 plan: &RenamePlan,
453) -> bool {
454 let target = renamed_key_name(&plan.key.name, &plan.new_name);
458 pre.equals_modulo_rename(post, &plan.key, &target, |s| remap_site(s, plan))
459}
460
461fn renamed_key_name(old: &str, new_segment: &str) -> String {
465 match old.rfind('.') {
466 Some(i) => format!("{}.{new_segment}", &old[..i]),
467 None => new_segment.to_string(),
468 }
469}
470
471fn key_segment(name: &str) -> &str {
474 name.rsplit('.').next().unwrap_or(name)
475}
476
477pub fn no_new_diagnostics(
481 pre: &[(PathBuf, String)],
482 post: &[(PathBuf, String)],
483) -> Result<(), String> {
484 let mut budget: HashMap<(&Path, &str), isize> = HashMap::new();
485 for (p, c) in pre {
486 *budget.entry((p.as_path(), c.as_str())).or_default() += 1;
487 }
488 for (p, c) in post {
489 let n = budget.entry((p.as_path(), c.as_str())).or_default();
490 *n -= 1;
491 if *n < 0 {
492 return Err(format!(
493 "rename would introduce `{c}` in {} — refused",
494 p.display()
495 ));
496 }
497 }
498 Ok(())
499}
500
501pub fn semantic_tokens_legend() -> tower_lsp::lsp_types::SemanticTokensLegend {
509 use tower_lsp::lsp_types::{SemanticTokenModifier, SemanticTokenType, SemanticTokensLegend};
510 SemanticTokensLegend {
511 token_types: vec![
512 SemanticTokenType::TYPE,
513 SemanticTokenType::FUNCTION,
514 SemanticTokenType::new("capability"),
515 SemanticTokenType::new("service"),
516 SemanticTokenType::new("agent"),
517 SemanticTokenType::new("provider"),
518 SemanticTokenType::VARIABLE,
521 SemanticTokenType::METHOD,
524 SemanticTokenType::PROPERTY,
527 SemanticTokenType::new("actor"),
530 SemanticTokenType::DECORATOR,
534 SemanticTokenType::new("messages"),
538 ],
539 token_modifiers: vec![
540 SemanticTokenModifier::DECLARATION,
541 SemanticTokenModifier::new("refined"),
542 SemanticTokenModifier::new("opaque"),
543 SemanticTokenModifier::new("platformNative"),
544 ],
545 }
546}
547
548fn token_type_index(kind: SymbolKind) -> u32 {
550 match kind {
551 SymbolKind::Type => 0,
552 SymbolKind::Fn => 1,
553 SymbolKind::Capability => 2,
554 SymbolKind::Service => 3,
555 SymbolKind::Agent => 4,
556 SymbolKind::Provider => 5,
557 SymbolKind::Method => 7,
559 SymbolKind::CapabilityOp => 7,
561 SymbolKind::Field => 8,
562 SymbolKind::Actor => 9,
564 SymbolKind::Handler => 7,
566 SymbolKind::Messages => 11,
568 }
569}
570
571const TOK_LOCAL: u32 = 6;
573
574const TOK_DECORATOR: u32 = 10;
576
577const MOD_DECLARATION: u32 = 1 << 0;
578const MOD_REFINED: u32 = 1 << 1;
579const MOD_OPAQUE: u32 = 1 << 2;
580const MOD_PLATFORM_NATIVE: u32 = 1 << 3;
581
582fn modifier_bits(m: bynk_check::index::SymbolModifiers) -> u32 {
583 (if m.refined { MOD_REFINED } else { 0 })
584 | (if m.opaque { MOD_OPAQUE } else { 0 })
585 | (if m.platform_native {
586 MOD_PLATFORM_NATIVE
587 } else {
588 0
589 })
590}
591
592pub fn semantic_tokens(
599 index: &ProjectIndex,
600 local_tokens: &[(Span, bool)],
601 decorator_tokens: &[Span],
602 path: &Path,
603 text: &str,
604 range: Option<Span>,
605) -> Vec<tower_lsp::lsp_types::SemanticToken> {
606 let in_scope = |span: Span| {
607 span.end <= text.len() && range.is_none_or(|r| span.end > r.start && span.start < r.end)
608 };
609 let mut raw: Vec<(Span, u32, u32)> = Vec::new();
610 for (key, entry) in &index.symbols {
611 let ty = token_type_index(key.kind);
612 let mods = modifier_bits(entry.modifiers);
613 if let Some(def) = &entry.def
614 && def.path == path
615 && in_scope(def.span)
616 {
617 raw.push((def.span, ty, mods | MOD_DECLARATION));
618 }
619 for site in &entry.refs {
620 if site.path == path && in_scope(site.span) {
621 raw.push((site.span, ty, mods));
622 }
623 }
624 }
625 for fr in &index.foreign_refs {
626 if fr.site.path == path && in_scope(fr.site.span) {
627 raw.push((
628 fr.site.span,
629 token_type_index(fr.kind),
630 modifier_bits(fr.modifiers),
631 ));
632 }
633 }
634 for &(span, is_decl) in local_tokens {
639 if in_scope(span) {
640 raw.push((span, TOK_LOCAL, if is_decl { MOD_DECLARATION } else { 0 }));
641 }
642 }
643 for &span in decorator_tokens {
648 if in_scope(span) {
649 raw.push((span, TOK_DECORATOR, 0));
650 }
651 }
652 raw.sort_by_key(|(span, _, _)| (span.start, span.end));
655 let mut data = Vec::with_capacity(raw.len());
656 let positions = crate::position::PositionMap::new(text);
660 let (mut prev_line, mut prev_start) = (0u32, 0u32);
661 for (span, token_type, token_modifiers_bitset) in raw {
662 let pos = positions.position(span.start);
663 let delta_line = pos.line - prev_line;
664 let delta_start = if delta_line == 0 {
665 pos.character - prev_start
666 } else {
667 pos.character
668 };
669 data.push(tower_lsp::lsp_types::SemanticToken {
670 delta_line,
671 delta_start,
672 length: text[span.range()].encode_utf16().count() as u32,
675 token_type,
676 token_modifiers_bitset,
677 });
678 prev_line = pos.line;
679 prev_start = pos.character;
680 }
681 data
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687 use bynk_check::index::{SymbolEntry, SymbolKind};
688
689 fn site(path: &str, start: usize, end: usize) -> SiteRef {
690 SiteRef {
691 path: PathBuf::from(path),
692 span: Span::new(start, end),
693 }
694 }
695
696 fn key(unit: &str, kind: SymbolKind, name: &str) -> SymbolKey {
697 SymbolKey {
698 unit: unit.into(),
699 kind,
700 name: name.into(),
701 }
702 }
703
704 fn index_with(entries: Vec<(SymbolKey, SiteRef, Vec<SiteRef>)>) -> ProjectIndex {
705 let mut index = ProjectIndex::default();
706 for (k, def, refs) in entries {
707 index.symbols.insert(
708 k,
709 SymbolEntry {
710 def: Some(def),
711 refs,
712 ..Default::default()
713 },
714 );
715 }
716 index
717 }
718
719 #[test]
720 fn new_name_validation() {
721 assert!(validate_new_name("Money2").is_ok());
722 assert!(validate_new_name("snake_case").is_ok());
723 assert!(validate_new_name("fn").is_err(), "keyword");
724 assert!(validate_new_name("two words").is_err());
725 assert!(validate_new_name("1abc").is_err());
726 assert!(validate_new_name("a.b").is_err());
727 assert!(validate_new_name("").is_err());
728 }
729
730 #[test]
731 fn apply_and_remap_agree() {
732 let text = "fn helper(x: Int) -> Int { helper(x) }";
735 let k = key("demo.a", SymbolKind::Fn, "helper");
736 let index = index_with(vec![(
737 k.clone(),
738 site("a.bynk", 3, 9),
739 vec![site("a.bynk", 27, 33)],
740 )]);
741 let plan = plan_rename(&index, Path::new("a.bynk"), 4, "do_it").unwrap();
742 let edited = apply_edits(text, &plan.edits[Path::new("a.bynk")], "do_it");
743 assert_eq!(edited, "fn do_it(x: Int) -> Int { do_it(x) }");
744
745 for (old, expected) in [
747 (site("a.bynk", 3, 9), "do_it"),
748 (site("a.bynk", 27, 33), "do_it"),
749 ] {
750 let new = remap_site(&old, &plan);
751 assert_eq!(&edited[new.span.range()], expected);
752 }
753 let unrelated = site("a.bynk", 34, 35); let new = remap_site(&unrelated, &plan);
756 assert_eq!(&edited[new.span.range()], "x");
757 }
758
759 #[test]
760 fn references_listing_orders_definition_first() {
761 let k = key("demo.a", SymbolKind::Type, "Money");
762 let index = index_with(vec![(
763 k,
764 site("a.bynk", 5, 10),
765 vec![site("b.bynk", 1, 6), site("a.bynk", 20, 25)],
766 )]);
767 let all = sites_for(&index, Path::new("b.bynk"), 3, true).unwrap();
768 assert_eq!(all.len(), 3);
769 assert_eq!(all[0].path, PathBuf::from("a.bynk"));
770 assert_eq!(all[0].span, Span::new(5, 10));
771 let without_decl = sites_for(&index, Path::new("b.bynk"), 3, false).unwrap();
772 assert_eq!(without_decl.len(), 2);
773 }
774
775 #[test]
776 fn rename_refuses_unindexed_positions_and_same_name() {
777 let k = key("demo.a", SymbolKind::Fn, "helper");
778 let index = index_with(vec![(k, site("a.bynk", 3, 9), vec![])]);
779 assert!(plan_rename(&index, Path::new("a.bynk"), 100, "x").is_err());
780 assert!(plan_rename(&index, Path::new("a.bynk"), 4, "helper").is_err());
781 }
782
783 #[test]
784 fn index_equality_detects_escape() {
785 let helper = key("demo.a", SymbolKind::Fn, "helper");
787 let money = key("demo.a", SymbolKind::Type, "Money");
788 let pre = index_with(vec![
789 (helper.clone(), site("a.bynk", 3, 9), vec![]),
790 (money.clone(), site("a.bynk", 50, 55), vec![]),
791 ]);
792 let plan = plan_rename(&pre, Path::new("a.bynk"), 4, "shadow").unwrap();
793
794 let honest = index_with(vec![
796 (
797 key("demo.a", SymbolKind::Fn, "shadow"),
798 site("a.bynk", 3, 9),
799 vec![],
800 ),
801 (money.clone(), site("a.bynk", 50, 55), vec![]),
802 ]);
803 assert!(index_unchanged_modulo_rename(&pre, &honest, &plan));
804
805 let escape = index_with(vec![
808 (
809 key("demo.a", SymbolKind::Fn, "shadow"),
810 site("a.bynk", 3, 9),
811 vec![site("a.bynk", 70, 76)],
812 ),
813 (money, site("a.bynk", 50, 55), vec![]),
814 ]);
815 assert!(!index_unchanged_modulo_rename(&pre, &escape, &plan));
816 }
817
818 #[test]
819 fn method_rename_edits_the_member_segment_and_remaps_the_compound_key() {
820 let bump = key("demo.a", SymbolKind::Method, "Counter.bump");
825 let other = key("demo.a", SymbolKind::Type, "Counter");
826 let pre = index_with(vec![
827 (other.clone(), site("a.bynk", 0, 5), vec![]),
828 (
830 bump.clone(),
831 site("a.bynk", 11, 15),
832 vec![site("a.bynk", 40, 44)],
833 ),
834 ]);
835
836 let plan = plan_rename(&pre, Path::new("a.bynk"), 12, "increment").unwrap();
838 assert_eq!(plan.key.name, "Counter.bump");
839 assert_eq!(plan.new_name, "increment");
840 assert!(plan_rename(&pre, Path::new("a.bynk"), 12, "bump").is_err());
842
843 let post = index_with(vec![
846 (other, site("a.bynk", 0, 5), vec![]),
847 (
848 key("demo.a", SymbolKind::Method, "Counter.increment"),
849 site("a.bynk", 11, 20),
850 vec![site("a.bynk", 45, 54)],
851 ),
852 ]);
853 assert!(
854 index_unchanged_modulo_rename(&pre, &post, &plan),
855 "compound key remaps to Counter.increment and segment-based delta lines the spans up"
856 );
857 }
858
859 #[test]
860 fn workspace_symbols_filters_and_orders() {
861 let index = index_with(vec![
862 (
863 key("demo.a", SymbolKind::Type, "Money"),
864 site("a.bynk", 5, 10),
865 vec![],
866 ),
867 (
868 key("demo.b", SymbolKind::Fn, "moneyMaker"),
869 site("b.bynk", 3, 13),
870 vec![],
871 ),
872 (
873 key("demo.a", SymbolKind::Fn, "helper"),
874 site("a.bynk", 40, 46),
875 vec![],
876 ),
877 ]);
878 let hits = workspace_symbols(&index, "money");
880 assert_eq!(
881 hits.iter()
882 .map(|(k, _)| k.name.as_str())
883 .collect::<Vec<_>>(),
884 vec!["Money", "moneyMaker"]
885 );
886 assert_eq!(workspace_symbols(&index, "").len(), 3);
888 assert!(workspace_symbols(&index, "nothing").is_empty());
889 }
890
891 #[test]
892 fn document_highlights_are_file_scoped() {
893 let k = key("demo.a", SymbolKind::Type, "Money");
894 let index = index_with(vec![(
895 k,
896 site("a.bynk", 5, 10),
897 vec![site("b.bynk", 1, 6), site("a.bynk", 20, 25)],
898 )]);
899 let highlights = document_highlights(&index, Path::new("a.bynk"), 7).unwrap();
901 assert_eq!(highlights.len(), 2);
902 assert!(highlights.iter().all(|s| s.path == Path::new("a.bynk")));
903 assert!(document_highlights(&index, Path::new("a.bynk"), 100).is_none());
905 }
906
907 #[test]
908 fn diagnostic_budget_allows_removals_refuses_additions() {
909 let pre = vec![
910 (PathBuf::from("a.bynk"), "bynk.x".to_string()),
911 (PathBuf::from("a.bynk"), "bynk.x".to_string()),
912 ];
913 let same = pre.clone();
914 assert!(no_new_diagnostics(&pre, &same).is_ok());
915 assert!(no_new_diagnostics(&pre, &pre[..1]).is_ok());
916 let mut more = pre.clone();
917 more.push((PathBuf::from("b.bynk"), "bynk.resolve.duplicate_fn".into()));
918 assert!(no_new_diagnostics(&pre, &more).is_err());
919 }
920
921 #[test]
927 fn legend_is_frozen() {
928 let legend = semantic_tokens_legend();
929 let types: Vec<&str> = legend.token_types.iter().map(|t| t.as_str()).collect();
930 assert_eq!(
931 types,
932 [
933 "type",
934 "function",
935 "capability",
936 "service",
937 "agent",
938 "provider",
939 "variable", "method", "property", "actor", "decorator", "messages", ]
946 );
947 let modifiers: Vec<&str> = legend.token_modifiers.iter().map(|m| m.as_str()).collect();
948 assert_eq!(
949 modifiers,
950 ["declaration", "refined", "opaque", "platformNative"]
951 );
952 }
953
954 #[test]
955 fn code_lenses_count_references_per_definition_in_the_file() {
956 let index = index_with(vec![
957 (
959 key("u", SymbolKind::Fn, "foo"),
960 site("a.bynk", 3, 6),
961 vec![site("a.bynk", 20, 23), site("b.bynk", 4, 7)],
962 ),
963 (
965 key("u", SymbolKind::Type, "Bar"),
966 site("a.bynk", 40, 43),
967 vec![],
968 ),
969 (
971 key("u", SymbolKind::Fn, "qux"),
972 site("b.bynk", 0, 3),
973 vec![],
974 ),
975 ]);
976 let lenses = code_lenses(&index, Path::new("a.bynk"));
977 assert_eq!(lenses.len(), 2, "two a.bynk defs get lenses");
978 assert_eq!((lenses[0].0.span.start, lenses[0].1.len()), (3, 2));
980 assert_eq!((lenses[1].0.span.start, lenses[1].1.len()), (40, 0));
981 assert!(code_lenses(&index, Path::new("c.bynk")).is_empty());
982 }
983
984 #[test]
985 fn call_hierarchy_groups_incoming_and_outgoing_by_symbol() {
986 use bynk_check::index::CallEdge;
987 let mut index = index_with(vec![
991 (key("u", SymbolKind::Fn, "a"), site("f.bynk", 3, 4), vec![]),
992 (
993 key("u", SymbolKind::Fn, "b"),
994 site("f.bynk", 40, 41),
995 vec![],
996 ),
997 (
998 key("u", SymbolKind::Fn, "c"),
999 site("f.bynk", 80, 81),
1000 vec![],
1001 ),
1002 ]);
1003 let edge = |caller: &str, cs: usize, ce: usize| CallEdge {
1004 caller: key("u", SymbolKind::Fn, caller),
1005 callee: key("u", SymbolKind::Fn, "c"),
1006 site: site("f.bynk", cs, ce),
1007 };
1008 index.calls = vec![edge("a", 10, 11), edge("a", 20, 21), edge("b", 50, 51)];
1009
1010 let into_c = incoming_calls(&index, &key("u", SymbolKind::Fn, "c"));
1011 assert_eq!(into_c.len(), 2);
1013 assert_eq!(
1014 (into_c[0].key.name.as_str(), into_c[0].sites.len()),
1015 ("a", 2)
1016 );
1017 assert_eq!(
1018 (into_c[1].key.name.as_str(), into_c[1].sites.len()),
1019 ("b", 1)
1020 );
1021
1022 let from_a = outgoing_calls(&index, &key("u", SymbolKind::Fn, "a"));
1023 assert_eq!(from_a.len(), 1);
1024 assert_eq!(
1025 (from_a[0].key.name.as_str(), from_a[0].sites.len()),
1026 ("c", 2)
1027 );
1028
1029 assert!(outgoing_calls(&index, &key("u", SymbolKind::Fn, "c")).is_empty());
1031 assert!(incoming_calls(&index, &key("u", SymbolKind::Fn, "ghost")).is_empty());
1032 }
1033
1034 #[test]
1035 fn implementations_lists_provider_defs_for_a_capability() {
1036 use bynk_check::index::ImplEdge;
1037 let mut index = index_with(vec![
1039 (
1040 key("u", SymbolKind::Capability, "Cap"),
1041 site("a.bynk", 10, 13),
1042 vec![],
1043 ),
1044 (
1045 key("u", SymbolKind::Provider, "P1"),
1046 site("a.bynk", 50, 52),
1047 vec![],
1048 ),
1049 (
1050 key("u", SymbolKind::Provider, "P2"),
1051 site("b.bynk", 5, 7),
1052 vec![],
1053 ),
1054 (
1055 key("u", SymbolKind::Capability, "Other"),
1056 site("a.bynk", 80, 85),
1057 vec![],
1058 ),
1059 ]);
1060 let edge = |provider: &str, file: &str, s: usize, e: usize| ImplEdge {
1061 capability: key("u", SymbolKind::Capability, "Cap"),
1062 provider: key("u", SymbolKind::Provider, provider),
1063 site: site(file, s, e),
1064 };
1065 index.impls = vec![edge("P1", "a.bynk", 30, 33), edge("P2", "b.bynk", 20, 23)];
1066
1067 let impls = implementations(&index, &key("u", SymbolKind::Capability, "Cap"));
1069 assert_eq!(impls.len(), 2);
1070 assert_eq!(
1071 (&impls[0].path, impls[0].span.start),
1072 (&PathBuf::from("a.bynk"), 50)
1073 );
1074 assert_eq!(
1075 (&impls[1].path, impls[1].span.start),
1076 (&PathBuf::from("b.bynk"), 5)
1077 );
1078
1079 assert!(implementations(&index, &key("u", SymbolKind::Capability, "Other")).is_empty());
1081 assert!(implementations(&index, &key("u", SymbolKind::Capability, "Ghost")).is_empty());
1082 }
1083
1084 #[test]
1085 fn capability_provider_lenses_pair_a_capability_with_its_providers() {
1086 use bynk_check::index::ImplEdge;
1087 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 key("u", SymbolKind::Capability, "Elsewhere"),
1112 site("b.bynk", 90, 99),
1113 vec![],
1114 ),
1115 ]);
1116 index.impls = vec![
1117 ImplEdge {
1118 capability: key("u", SymbolKind::Capability, "Cap"),
1119 provider: key("u", SymbolKind::Provider, "P1"),
1120 site: site("a.bynk", 30, 33),
1121 },
1122 ImplEdge {
1123 capability: key("u", SymbolKind::Capability, "Cap"),
1124 provider: key("u", SymbolKind::Provider, "P2"),
1125 site: site("b.bynk", 20, 23),
1126 },
1127 ];
1128
1129 let lenses = capability_provider_lenses(&index, Path::new("a.bynk"));
1130 assert_eq!(lenses.len(), 1);
1132 assert_eq!(lenses[0].0.span.start, 10);
1133 assert_eq!(lenses[0].1.len(), 2, "Cap has two providers");
1134 assert!(capability_provider_lenses(&index, Path::new("c.bynk")).is_empty());
1136 }
1137
1138 #[test]
1139 fn refinement_family_lenses_group_refined_types_by_builtin_base() {
1140 use bynk_check::index::RefineEdge;
1141 let mut index = index_with(vec![
1144 (
1145 key("u", SymbolKind::Type, "Email"),
1146 site("a.bynk", 10, 15),
1147 vec![],
1148 ),
1149 (
1150 key("u", SymbolKind::Type, "UserId"),
1151 site("a.bynk", 40, 46),
1152 vec![],
1153 ),
1154 (
1155 key("u", SymbolKind::Type, "Slug"),
1156 site("b.bynk", 5, 9),
1157 vec![],
1158 ),
1159 (
1160 key("u", SymbolKind::Type, "Age"),
1161 site("a.bynk", 70, 73),
1162 vec![],
1163 ),
1164 (
1165 key("u", SymbolKind::Type, "Order"),
1166 site("a.bynk", 90, 95),
1167 vec![],
1168 ),
1169 ]);
1170 let refine = |name: &str, base: BaseType| RefineEdge {
1171 base,
1172 ty: key("u", SymbolKind::Type, name),
1173 };
1174 index.refinements = vec![
1175 refine("Email", BaseType::String),
1176 refine("UserId", BaseType::String),
1177 refine("Slug", BaseType::String),
1178 refine("Age", BaseType::Int),
1179 ];
1180
1181 let lenses = refinement_family_lenses(&index, Path::new("a.bynk"));
1182 assert_eq!(lenses.len(), 2);
1185 assert_eq!(
1187 (lenses[0].0.span.start, lenses[0].1),
1188 (10, BaseType::String)
1189 );
1190 assert_eq!(lenses[0].2.len(), 3, "the String family spans 3 types");
1191 assert_eq!(lenses[1].0.span.start, 40);
1192 assert!(refinement_family_lenses(&index, Path::new("c.bynk")).is_empty());
1194 }
1195
1196 #[test]
1197 fn type_definitions_named_collects_type_defs_by_bare_name() {
1198 let index = index_with(vec![
1200 (
1201 key("a", SymbolKind::Type, "Order"),
1202 site("a.bynk", 10, 15),
1203 vec![],
1204 ),
1205 (
1206 key("b", SymbolKind::Type, "Order"),
1207 site("b.bynk", 4, 9),
1208 vec![],
1209 ),
1210 (
1211 key("a", SymbolKind::Fn, "Order"),
1212 site("a.bynk", 40, 45),
1213 vec![],
1214 ),
1215 ]);
1216 let defs = type_definitions_named(&index, "Order");
1218 assert_eq!(defs.len(), 2);
1219 assert_eq!(
1220 (&defs[0].path, defs[0].span.start),
1221 (&PathBuf::from("a.bynk"), 10)
1222 );
1223 assert_eq!(
1224 (&defs[1].path, defs[1].span.start),
1225 (&PathBuf::from("b.bynk"), 4)
1226 );
1227 assert!(type_definitions_named(&index, "Nope").is_empty());
1229 }
1230
1231 fn doc_scope_with(entries: Vec<(&str, Vec<&str>)>) -> HashMap<String, Vec<String>> {
1232 entries
1233 .into_iter()
1234 .map(|(unit, scope)| {
1235 (
1236 unit.to_string(),
1237 scope.into_iter().map(String::from).collect(),
1238 )
1239 })
1240 .collect()
1241 }
1242
1243 #[test]
1244 fn resolve_doc_link_finds_a_bare_local_hit() {
1245 let index = index_with(vec![(
1246 key("ratelimit", SymbolKind::Agent, "Limiter"),
1247 site("ratelimit.bynk", 10, 17),
1248 vec![],
1249 )]);
1250 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit"])]);
1251 let def = resolve_doc_link(&index, &scope, "ratelimit", "Limiter").unwrap();
1252 assert_eq!(def.path, PathBuf::from("ratelimit.bynk"));
1253 }
1254
1255 #[test]
1256 fn resolve_doc_link_finds_a_dotted_local_hit() {
1257 let index = index_with(vec![(
1258 key("ratelimit", SymbolKind::Field, "RateView.remaining"),
1259 site("ratelimit.bynk", 20, 29),
1260 vec![],
1261 )]);
1262 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit"])]);
1263 let def = resolve_doc_link(&index, &scope, "ratelimit", "RateView.remaining").unwrap();
1264 assert_eq!(def.path, PathBuf::from("ratelimit.bynk"));
1265 }
1266
1267 #[test]
1268 fn resolve_doc_link_follows_the_uses_chain() {
1269 let index = index_with(vec![(
1273 key("window", SymbolKind::Fn, "decide"),
1274 site("window.bynk", 5, 11),
1275 vec![],
1276 )]);
1277 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit", "window"])]);
1278 let def = resolve_doc_link(&index, &scope, "ratelimit", "decide").unwrap();
1279 assert_eq!(def.path, PathBuf::from("window.bynk"));
1280 }
1281
1282 #[test]
1283 fn resolve_doc_link_follows_the_consumes_chain_after_uses() {
1284 let index = index_with(vec![(
1285 key("platform", SymbolKind::Capability, "Clock"),
1286 site("platform.bynk", 0, 5),
1287 vec![],
1288 )]);
1289 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit", "window", "platform"])]);
1292 let def = resolve_doc_link(&index, &scope, "ratelimit", "Clock").unwrap();
1293 assert_eq!(def.path, PathBuf::from("platform.bynk"));
1294 }
1295
1296 #[test]
1297 fn resolve_doc_link_is_none_when_nothing_in_scope_matches() {
1298 let index = index_with(vec![(
1299 key("ratelimit", SymbolKind::Agent, "Limiter"),
1300 site("ratelimit.bynk", 10, 17),
1301 vec![],
1302 )]);
1303 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit"])]);
1304 assert!(resolve_doc_link(&index, &scope, "ratelimit", "Nope").is_none());
1305 }
1306
1307 #[test]
1308 fn resolve_doc_link_is_none_for_a_synthetic_unit_never_in_scope() {
1309 let index = ProjectIndex::default();
1314 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit", "bynk"])]);
1315 assert!(resolve_doc_link(&index, &scope, "ratelimit", "Clock.now").is_none());
1316 }
1317
1318 #[test]
1319 fn resolve_doc_link_is_none_when_the_owner_unit_has_no_scope_entry() {
1320 let index = ProjectIndex::default();
1321 let scope = doc_scope_with(vec![]);
1322 assert!(resolve_doc_link(&index, &scope, "ratelimit", "Limiter").is_none());
1323 }
1324
1325 #[test]
1326 fn resolve_doc_link_never_guesses_an_ambiguous_first_matching_unit() {
1327 let index = index_with(vec![
1332 (
1333 key("ratelimit", SymbolKind::Type, "Order"),
1334 site("ratelimit.bynk", 10, 15),
1335 vec![],
1336 ),
1337 (
1338 key("ratelimit", SymbolKind::Fn, "Order"),
1339 site("ratelimit.bynk", 40, 45),
1340 vec![],
1341 ),
1342 (
1343 key("window", SymbolKind::Type, "Order"),
1344 site("window.bynk", 0, 5),
1345 vec![],
1346 ),
1347 ]);
1348 let scope = doc_scope_with(vec![("ratelimit", vec!["ratelimit", "window"])]);
1349 assert!(resolve_doc_link(&index, &scope, "ratelimit", "Order").is_none());
1350 }
1351
1352 #[test]
1353 fn named_type_target_unwraps_single_param_containers() {
1354 use bynk_check::checker::{NamedKind, Types};
1355 use bynk_syntax::ast::BaseType;
1356 let tys = &Types::new();
1357 let order = || {
1358 tys.intern(Ty::Named {
1359 name: "Order".into(),
1360 kind: NamedKind::Record,
1361 args: Vec::new(),
1362 })
1363 };
1364 let target = |t| named_type_target(t, tys);
1365 assert_eq!(target(order()).as_deref(), Some("Order"));
1366 assert_eq!(
1367 target(tys.intern(Ty::Option(order()))).as_deref(),
1368 Some("Order")
1369 );
1370 let effect = tys.intern(Ty::Effect(order()));
1372 assert_eq!(
1373 target(tys.intern(Ty::List(effect))).as_deref(),
1374 Some("Order")
1375 );
1376 assert_eq!(target(tys.intern(Ty::Base(BaseType::Int))), None);
1378 assert_eq!(target(tys.intern(Ty::Result(order(), order()))), None);
1379 assert_eq!(target(tys.intern(Ty::Unit)), None);
1380 }
1381
1382 #[test]
1383 fn tokens_are_delta_encoded_with_modifier_bitsets() {
1384 let text = "type Age = Int\nfn f(a: Age) -> Age {}\n";
1387 let mut index = index_with(vec![(
1388 key("shop", SymbolKind::Type, "Age"),
1389 site("a.bynk", 5, 8),
1390 vec![site("a.bynk", 23, 26), site("a.bynk", 31, 34)],
1391 )]);
1392 index
1393 .symbols
1394 .get_mut(&key("shop", SymbolKind::Type, "Age"))
1395 .unwrap()
1396 .modifiers = bynk_check::index::SymbolModifiers {
1397 refined: true,
1398 ..Default::default()
1399 };
1400 let tokens = semantic_tokens(&index, &[], &[], Path::new("a.bynk"), text, None);
1401 assert_eq!(tokens.len(), 3);
1402 assert_eq!(
1404 (
1405 tokens[0].delta_line,
1406 tokens[0].delta_start,
1407 tokens[0].length
1408 ),
1409 (0, 5, 3)
1410 );
1411 assert_eq!(tokens[0].token_type, 0);
1412 assert_eq!(tokens[0].token_modifiers_bitset, 0b0011);
1413 assert_eq!(
1415 (
1416 tokens[1].delta_line,
1417 tokens[1].delta_start,
1418 tokens[1].length
1419 ),
1420 (1, 8, 3)
1421 );
1422 assert_eq!(tokens[1].token_modifiers_bitset, 0b0010);
1423 assert_eq!(
1425 (
1426 tokens[2].delta_line,
1427 tokens[2].delta_start,
1428 tokens[2].length
1429 ),
1430 (0, 8, 3)
1431 );
1432 }
1433
1434 #[test]
1435 fn foreign_refs_emit_tokens_and_range_filters() {
1436 let text = "given Kv {\n Kv.get(k)\n}\n";
1437 let mut index = ProjectIndex::default();
1438 index.foreign_refs.push(bynk_check::index::ForeignRef {
1439 site: site("a.bynk", 6, 8),
1440 kind: SymbolKind::Capability,
1441 modifiers: bynk_check::index::SymbolModifiers {
1442 platform_native: true,
1443 ..Default::default()
1444 },
1445 });
1446 index.foreign_refs.push(bynk_check::index::ForeignRef {
1447 site: site("a.bynk", 13, 15),
1448 kind: SymbolKind::Capability,
1449 modifiers: bynk_check::index::SymbolModifiers {
1450 platform_native: true,
1451 ..Default::default()
1452 },
1453 });
1454 let all = semantic_tokens(&index, &[], &[], Path::new("a.bynk"), text, None);
1455 assert_eq!(all.len(), 2);
1456 assert_eq!(all[0].token_type, 2); assert_eq!(all[0].token_modifiers_bitset, 0b1000); let ranged = semantic_tokens(
1460 &index,
1461 &[],
1462 &[],
1463 Path::new("a.bynk"),
1464 text,
1465 Some(Span::new(0, 10)),
1466 );
1467 assert_eq!(ranged.len(), 1);
1468 assert!(semantic_tokens(&index, &[], &[], Path::new("b.bynk"), text, None).is_empty());
1470 assert!(
1471 semantic_tokens(
1472 &ProjectIndex::default(),
1473 &[],
1474 &[],
1475 Path::new("a.bynk"),
1476 text,
1477 None
1478 )
1479 .is_empty()
1480 );
1481 }
1482}