Skip to main content

bynk_lsp/
index_queries.rs

1//! v0.25 (ADR 0053): pure queries over the project binding index.
2//!
3//! Everything here is a pure function over [`ProjectIndex`] + analysed
4//! snapshot texts — the unit-testable core behind `textDocument/references`
5//! and `rename`/`prepareRename`. Transport-side handlers in `main.rs` only
6//! convert positions and package results.
7//!
8//! Rename is validated two ways, both correct-by-construction:
9//! 1. **Collisions** — apply the edits to an overlay, re-run
10//!    `diagnose_project`, refuse if a new diagnostic appears
11//!    ([`no_new_diagnostics`]).
12//! 2. **Capture/escape** — re-analysis alone misses silent re-binding
13//!    (declared fns shadow fn-typed locals in call position), so the
14//!    re-built index must equal the pre-index *modulo the rename*
15//!    ([`index_unchanged_modulo_rename`]).
16
17use 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
25/// Definition first, then references — the `references` surface.
26pub 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); // definition is always first.
36    }
37    Some(sites)
38}
39
40/// The definition site for the symbol at the cursor (an index-backed,
41/// binding-correct go-to-definition).
42pub 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
52/// `prepareRename`: the renameable range under the cursor, or `None` for
53/// out-of-scope symbols (locals, unit names) —
54/// the request is refused rather than falling through to a partial rename.
55pub 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
63/// v0.26 rider (ADR 0055): `workspace/symbol` — every index definition whose
64/// name contains the query, case-insensitive (an empty query lists all),
65/// sorted by (name, unit) for a stable order.
66pub 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
81/// v0.33 (ADR 0066): `codeLens` — one reference-count lens per top-level
82/// definition in `path`, as `(def site, reference sites)`. The count is
83/// `refs.len()`; the reference sites feed the `showReferences` action. Sorted
84/// by definition position (a stable, top-to-bottom lens order).
85pub 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
98/// v0.127 (editor-currency slice 6): a provider-count lens per `capability`
99/// definition in `path`, as `(capability def, provider def sites)`. The count is
100/// `providers.len()`; the provider sites feed the same `showReferences` peek the
101/// reference lens uses. Reuses the `implementations` relation (the `impls_of`
102/// edge behind go-to-implementation), so a capability with no provider yields no
103/// lens. Sorted by definition position, alongside the reference lenses.
104pub 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
125/// v0.129 (#259): a refinement-family lens per refined/opaque `Type` definition
126/// in `path`, as `(type def, base, family def sites)` — every refined/opaque type
127/// (and plain alias) declared over the same builtin `base`, across the project.
128/// The family includes the type itself, so a lens is emitted only for a family of
129/// **≥ 2** (a lone refinement has nothing to navigate to — no lens, mirroring
130/// `capability_provider_lenses`). Sorted by definition position, alongside the
131/// reference lenses.
132pub 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
159/// v0.34 (ADR 0067): one end of a call-hierarchy relation — the related
160/// symbol (`key` + its definition site) and the call sites linking it to the
161/// queried symbol. For incoming calls `key` is a caller and `sites` are where
162/// it calls the queried symbol; for outgoing, `key` is a callee and `sites`
163/// are where the queried symbol calls it. The sites double as the LSP
164/// `fromRanges` (identical for both directions).
165pub struct CallRelation<'a> {
166    pub key: &'a SymbolKey,
167    pub def: &'a SiteRef,
168    pub sites: Vec<&'a SiteRef>,
169}
170
171/// v0.34 (ADR 0067): `prepareCallHierarchy` — the symbol under the cursor and
172/// its definition site (the goto-def resolution; an item is anchored on the
173/// definition). `None` for out-of-scope positions.
174pub 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
182/// Group `edges` by the key returned by `pick`, attach each grouped symbol's
183/// definition, and collect the call sites — the shared core of incoming and
184/// outgoing calls. Groups with no indexed definition are dropped (defensive;
185/// every call-edge endpoint is an index symbol by construction). Groups are
186/// ordered by definition position for a stable, top-to-bottom listing.
187fn 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
208/// v0.34 (ADR 0067): `callHierarchy/incomingCalls` — the callers of `key`,
209/// each with the call sites at which it calls `key`.
210pub 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
214/// v0.34 (ADR 0067): `callHierarchy/outgoingCalls` — what `key` calls, each
215/// with the call sites within `key` at which the callee is called.
216pub 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
220/// v0.35 (ADR 0068): `textDocument/implementation` — the definition sites of
221/// every provider implementing the capability `key`, sorted by definition
222/// position. Empty for a non-capability or unknown key (the request then falls
223/// through; goto-def still serves the reverse, provider → capability).
224pub 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
234/// Slice 6: `textDocument/typeDefinition` — the definition site(s) of the type
235/// named `name` (a `Type` symbol). The checker's `Ty::Named.name` and the index
236/// both use bare names, so this is a bare-name match; a name shared across units
237/// yields several locations (the LSP-conventional resolution — the client lets
238/// the user choose). Sorted by definition position.
239pub 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
251/// #848: the flat compound-name member kinds (`"Owner.member"`, built by
252/// `bynk-emit`'s `assemble_index`). A dotted doc-link name matches these
253/// verbatim — never split into owner/member.
254const DOC_LINK_MEMBER_KINDS: &[SymbolKind] = &[
255    SymbolKind::Field,
256    SymbolKind::Method,
257    SymbolKind::CapabilityOp,
258    SymbolKind::Handler,
259];
260
261/// #848: the top-level kinds a *bare* doc-link name matches.
262const 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
272/// #848: resolve one intra-doc-link candidate name (from
273/// `bynk_ide::symbols::scan_doc_link_candidates`) against `owner_unit`'s
274/// doc-link scope order in `doc_scope` — the unit itself, then its `uses`
275/// targets, then its `consumes` targets (mirrors
276/// `bynk_check::index::IndexBuilder::qualify_with`'s bare-name
277/// qualification).
278///
279/// A `name` containing `.` matches the flat compound member kinds
280/// (`DOC_LINK_MEMBER_KINDS`, private below) verbatim; otherwise it matches the
281/// bare top-level kinds (`DOC_LINK_BARE_KINDS`). The **first** scope-order unit
282/// with any match decides the answer: more than one candidate there is
283/// unresolved (no `kind@` disambiguation in this increment — convention-only
284/// resolution) rather than a guess, even if a later unit in scope order would
285/// answer unambiguously. Synthetic (first-party) units are never in
286/// `index.symbols` (their defs are dropped at assembly), so a name that only
287/// matches through one — e.g. `Clock.now` — naturally comes back `None`,
288/// exactly like any other unresolved name.
289pub 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, // ambiguous in the first matching unit — never guess
319        }
320    }
321    None
322}
323
324/// The user-declared type a value's type points at, for go-to-type-definition:
325/// a `Named` directly, or the element of a single-parameter container
326/// (`Option`/`Effect`/`List`/`HttpResult`) unwrapped to it. Built-in, function,
327/// actor, and two-parameter (`Result`/`Map`) types have no single
328/// type-declaration target and yield `None`.
329pub 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
339/// v0.26 rider (ADR 0055): `documentHighlight` — the symbol-at-cursor's
340/// occurrences within that same file (the `references` query, file-scoped).
341/// The index does not distinguish read from write references, so the LSP
342/// layer omits the highlight `kind`.
343pub 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/// A planned rename: every name-segment edit, grouped per file, spans
353/// ascending. The definition site is edited along with every reference.
354#[derive(Debug, Clone)]
355pub struct RenamePlan {
356    pub key: SymbolKey,
357    pub new_name: String,
358    pub edits: BTreeMap<PathBuf, Vec<Span>>,
359}
360
361/// Build the rename plan for the symbol at the cursor. Errors are
362/// human-readable strings surfaced as LSP request failures.
363pub 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
394/// A new name must lex as exactly one identifier (keywords lex as their own
395/// token kinds, so they fail this check).
396pub 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
410/// Apply one file's edits (spans ascending) to its snapshot text.
411pub 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
423/// The post-edit position of a pre-edit site — rename edits shift spans
424/// within edited files. An edited span maps to the new name's span.
425pub fn remap_site(site: &SiteRef, plan: &RenamePlan) -> SiteRef {
426    let Some(spans) = plan.edits.get(&site.path) else {
427        return site.clone();
428    };
429    // The edit replaces the member segment only (`"m"` of `"Type.m"`), so the
430    // length delta is against that segment, not the whole compound key name.
431    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
445/// Validator (2): the re-built index must equal the pre-index modulo the
446/// rename — every other symbol's reference set identical (after remapping
447/// shifted spans), the renamed symbol's sites exactly the edited ones.
448/// Catches silent re-binding (capture/escape) that produces no diagnostic.
449pub fn index_unchanged_modulo_rename(
450    pre: &ProjectIndex,
451    post: &ProjectIndex,
452    plan: &RenamePlan,
453) -> bool {
454    // v0.36 (ADR 0069): a member key carries a compound name (`"Type.method"`),
455    // but the edit replaces only the member segment — so the post-rename key is
456    // the prefix plus the new segment, not the bare new name.
457    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
461/// The post-rename value of a (possibly compound) key name: for a member key
462/// `"Type.method"`, replace the segment after the last `.`; for a bare name,
463/// the new name as-is.
464fn 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
471/// The member segment of a (possibly compound) key name — the text the rename
472/// actually edits (every site span covers exactly this).
473fn key_segment(name: &str) -> &str {
474    name.rsplit('.').next().unwrap_or(name)
475}
476
477/// Validator (1): refuse when the edited project carries a diagnostic the
478/// original did not — compared as per-(file, category) counts, robust to
479/// span shifts. Removals are tolerated; additions refuse.
480pub 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
501// -- v0.28 (ADR 0057): semantic tokens --
502
503/// The frozen semantic-tokens legend. **Array order is the wire encoding**
504/// (clients index into these arrays): entries are append-only, never
505/// reordered — pinned by the legend-stability test. Token types: standard
506/// where faithful (`type`, `function`), custom for the Bynk-distinctive
507/// kinds (`capability`, `service`, `agent`, `provider`).
508pub 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            // v0.31 (ADR 0064): local bindings + params. Standard LSP type —
519            // VS Code themes it by default, no extension declaration needed.
520            SemanticTokenType::VARIABLE,
521            // v0.36 (ADR 0069): instance methods. Appended (never reordered) so
522            // existing legend indices are unchanged. Standard LSP type.
523            SemanticTokenType::METHOD,
524            // v0.36 (ADR 0069, slice 2): record fields. Appended. Standard LSP
525            // type. (Capability ops reuse `method` — they're operation calls.)
526            SemanticTokenType::PROPERTY,
527            // v0.45: actor declarations. Appended at index 9 (never reordered).
528            // Custom type — the VS Code extension declares it in package.json.
529            SemanticTokenType::new("actor"),
530            // v0.140 (ADR 0163): handler-position annotations (`@cache` and its
531            // argument labels). Appended at index 10. Standard LSP type — VS Code
532            // themes `decorator` by default.
533            SemanticTokenType::DECORATOR,
534            // message-bundles slice 1: `messages <tag> { ... }` bundles.
535            // Appended at index 11 (never reordered). Custom type — the VS
536            // Code extension declares it in package.json.
537            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
548/// Legend indices/bits — must mirror [`semantic_tokens_legend`]'s order.
549fn 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        // 6 is `variable` (locals; TOK_LOCAL); methods append at 7.
558        SymbolKind::Method => 7,
559        // v0.36 slice 2: ops reuse `method` (7); fields append `property` at 8.
560        SymbolKind::CapabilityOp => 7,
561        SymbolKind::Field => 8,
562        // v0.45: actors append `actor` at 9.
563        SymbolKind::Actor => 9,
564        // #304: agent handlers reuse `method` (7), same as capability ops.
565        SymbolKind::Handler => 7,
566        // message-bundles slice 1: `messages` bundles append `messages` at 11.
567        SymbolKind::Messages => 11,
568    }
569}
570
571/// Legend index of the `variable` token type (locals; ADR 0064).
572const TOK_LOCAL: u32 = 6;
573
574/// Legend index of the `decorator` token type (handler annotations; ADR 0163).
575const 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
592/// Semantic tokens for `path`, delta-encoded over the frozen legend —
593/// a pure read of the cached index's two sources: `symbols` (user
594/// defs+refs; a def site carries `declaration`) and `foreign_refs`
595/// (first-party references). `range` (byte offsets into `text`, the
596/// analysed snapshot) filters to overlapping tokens for the `…/range`
597/// request; `None` is the full document.
598pub 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    // v0.31 (ADR 0064): local bindings + their uses (precomputed by the caller
635    // via `locals_nav`, so this stays free of that dependency for the
636    // `#[path]`-include tests). Disjoint from the index tokens — locals are
637    // never top-level symbols — so they merge into the same sorted stream.
638    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    // v0.140 (ADR 0163): handler-annotation name + argument-label spans (precomputed
644    // by the caller from the parsed unit, keeping this a parse-free index read).
645    // Disjoint from the index/local tokens — annotations are neither symbols nor
646    // locals — so they merge into the same sorted stream.
647    for &span in decorator_tokens {
648        if in_scope(span) {
649            raw.push((span, TOK_DECORATOR, 0));
650        }
651    }
652    // Name segments never overlap (the index invariant), so a position
653    // sort fully determines the protocol's relative encoding.
654    raw.sort_by_key(|(span, _, _)| (span.start, span.end));
655    let mut data = Vec::with_capacity(raw.len());
656    // One line index for the whole snapshot: a `semanticTokens/full` request
657    // emits a position per token, so scanning from byte 0 each time is O(n²)
658    // (#732). Build it once and binary-search per token instead.
659    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            // The protocol counts in the negotiated encoding (UTF-16, as
673            // positions are) — not bytes.
674            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        // text: "fn helper(x: Int) -> Int { helper(x) }"
733        //        3..9 def                  27..33 ref
734        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        // Remap maps both old sites onto the new spellings.
746        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        // An unedited later site shifts by the accumulated delta.
754        let unrelated = site("a.bynk", 34, 35); // `x` argument
755        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        // Pre: `helper` has no references; some other symbol unchanged.
786        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        // Post (honest): def renamed in place, still no refs.
795        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        // Post (escape): a site that used to bind elsewhere now resolves to
806        // the renamed symbol — an extra reference appears.
807        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        // v0.36: a method key is compound (`"Counter.bump"`), but the edit
821        // touches only the `bump` segment — so the plan's new name is the bare
822        // segment, the post key is `"Counter.increment"`, and the span delta is
823        // against the segment length (4), not the compound length (12).
824        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            // def `bump` at 11..15, one call at 40..44.
829            (
830                bump.clone(),
831                site("a.bynk", 11, 15),
832                vec![site("a.bynk", 40, 44)],
833            ),
834        ]);
835
836        // Cursor on the def segment; rename to a longer name.
837        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        // Renaming to the same segment is refused (segment-aware, not key-aware).
841        assert!(plan_rename(&pre, Path::new("a.bynk"), 12, "bump").is_err());
842
843        // Honest post: the compound key becomes `Counter.increment`; the def
844        // grows in place (11..20) and the call shifts by +5 (45..54).
845        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        // Case-insensitive substring match.
879        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        // Empty query lists everything, (name, unit)-ordered.
887        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        // From a.bynk: the definition + the in-file reference, not b.bynk's.
900        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        // No symbol at the cursor → None.
904        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    // -- v0.28 (ADR 0057): semantic tokens --
922
923    /// The legend's array order IS the wire encoding: this test freezes it.
924    /// New entries APPEND — a failure here means a silent recolour of every
925    /// client; never fix it by reordering.
926    #[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",  // v0.31 (ADR 0064): locals — appended, never reordered
940                "method",    // v0.36 (ADR 0069): instance methods — appended
941                "property",  // v0.36 (ADR 0069, slice 2): record fields — appended
942                "actor",     // v0.45: actor declarations — appended
943                "decorator", // v0.140 (ADR 0163): handler annotations — appended
944                "messages",  // message-bundles slice 1 (#859): messages bundles — appended
945            ]
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            // `foo` defined in a.bynk with two references.
958            (
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            // `Bar` defined in a.bynk with no references (a 0-ref lens).
964            (
965                key("u", SymbolKind::Type, "Bar"),
966                site("a.bynk", 40, 43),
967                vec![],
968            ),
969            // `qux` defined in another file — no lens for a.bynk.
970            (
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        // Sorted by def position: foo (3..6) before Bar (40..43).
979        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        // `a` and `b` both call `c`; `a` calls `c` twice. So `c`'s incoming
988        // groups by caller (a with two sites, b with one), and `a`'s outgoing
989        // is the single callee `c`.
990        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        // Sorted by caller def position: a (3) before b (40).
1012        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        // `c` calls nothing; an unknown key yields nothing.
1030        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        // `Cap` is provided by `P1` and `P2`; `Other` (a capability) has none.
1038        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        // Provider defs, sorted by position: P1 (a.bynk:50) before P2 (b.bynk:5).
1068        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        // A capability with no providers, and an unknown key, yield nothing.
1080        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        // `Cap` (defined in a.bynk) is provided by `P1`/`P2`; `Other` (a.bynk)
1088        // has no provider — so no lens; `Elsewhere` lives in b.bynk — no a.bynk lens.
1089        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        // Only `Cap` qualifies: `Other` has no providers, `Elsewhere` is off-file.
1131        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        // Off-file query and a provider-less file yield nothing.
1135        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        // Email/UserId (a.bynk) + Slug (b.bynk) refine String — a family of 3;
1142        // Age refines Int alone (no family); Order is a record (no base).
1143        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        // Email + UserId qualify (the String family has 3 members, ≥ 2). `Age` is
1183        // alone over Int — no lens. `Order` has no base. `Slug` is off-file.
1184        assert_eq!(lenses.len(), 2);
1185        // Sorted by def position: Email (10) before UserId (40).
1186        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        // Off-file query yields nothing.
1193        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        // Two units each declare an `Order` type; a fn shares the name.
1199        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        // Both `Type` defs (not the fn), sorted by position.
1217        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        // An unknown type name yields nothing.
1228        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        // `decide` isn't declared in `ratelimit`, only in `window`, which
1270        // `ratelimit` `uses` — the scope order (built the same way
1271        // `doc_scope` is assembled) puts `window` after the local unit.
1272        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        // `uses` targets come before `consumes` targets in scope order; a name
1290        // only found via `consumes` still resolves, just later in the search.
1291        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        // A synthetic (first-party) unit's symbols never make it into
1310        // `index.symbols` at all (their defs are dropped at assembly), so a
1311        // name that only exists through one comes back unresolved even
1312        // though `bynk` is a `consumes` target in scope.
1313        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        // Two different candidate kinds share the bare name `Order` in the
1328        // *first* scope-order unit — even though a later unit in scope order
1329        // would resolve unambiguously, the ambiguity in the first matching
1330        // unit wins: unresolved, never a guess.
1331        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        // Nested single-param containers unwrap all the way.
1371        let effect = tys.intern(Ty::Effect(order()));
1372        assert_eq!(
1373            target(tys.intern(Ty::List(effect))).as_deref(),
1374            Some("Order")
1375        );
1376        // Built-in, two-parameter, and unit types have no single target.
1377        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        // text:  line 0: "type Age = Int"   (def `Age` at 5..8, refined)
1385        //        line 1: "fn f(a: Age) ..." (ref `Age` at 23..26)
1386        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        // Def: line 0 char 5, length 3, type `type` (0), declaration|refined.
1403        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        // First ref: next line, char 8 (absolute — line changed), refined only.
1414        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        // Second ref: same line, char delta from the previous token's start.
1424        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); // capability
1457        assert_eq!(all[0].token_modifiers_bitset, 0b1000); // platformNative
1458        // Range covering only line 0 keeps only the first token.
1459        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        // Other files and empty indexes yield nothing.
1469        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}