Skip to main content

bynkc_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;
21use bynk_check::index::{ProjectIndex, SiteRef, SymbolKey, SymbolKind};
22use bynk_syntax::span::Span;
23
24/// Definition first, then references — the `references` surface.
25pub fn sites_for<'a>(
26    index: &'a ProjectIndex,
27    path: &Path,
28    offset: usize,
29    include_declaration: bool,
30) -> Option<Vec<&'a SiteRef>> {
31    let (key, _) = index.symbol_at(path, offset)?;
32    let mut sites = index.sites(key);
33    if !include_declaration && !sites.is_empty() {
34        sites.remove(0); // definition is always first.
35    }
36    Some(sites)
37}
38
39/// The definition site for the symbol at the cursor (an index-backed,
40/// binding-correct go-to-definition).
41pub fn definition_at<'a>(
42    index: &'a ProjectIndex,
43    path: &Path,
44    offset: usize,
45) -> Option<(&'a SymbolKey, &'a SiteRef)> {
46    let (key, _) = index.symbol_at(path, offset)?;
47    let entry = index.symbols.get(key)?;
48    Some((key, entry.def.as_ref()?))
49}
50
51/// `prepareRename`: the renameable range under the cursor, or `None` for
52/// out-of-scope symbols (locals, unit names) —
53/// the request is refused rather than falling through to a partial rename.
54pub fn prepare_rename<'a>(
55    index: &'a ProjectIndex,
56    path: &Path,
57    offset: usize,
58) -> Option<(&'a SymbolKey, &'a SiteRef)> {
59    index.symbol_at(path, offset)
60}
61
62/// v0.26 rider (ADR 0055): `workspace/symbol` — every index definition whose
63/// name contains the query, case-insensitive (an empty query lists all),
64/// sorted by (name, unit) for a stable order.
65pub fn workspace_symbols<'a>(
66    index: &'a ProjectIndex,
67    query: &str,
68) -> Vec<(&'a SymbolKey, &'a SiteRef)> {
69    let q = query.to_lowercase();
70    let mut out: Vec<_> = index
71        .symbols
72        .iter()
73        .filter(|(k, _)| q.is_empty() || k.name.to_lowercase().contains(&q))
74        .filter_map(|(k, e)| e.def.as_ref().map(|d| (k, d)))
75        .collect();
76    out.sort_by(|a, b| (&a.0.name, &a.0.unit).cmp(&(&b.0.name, &b.0.unit)));
77    out
78}
79
80/// v0.33 (ADR 0066): `codeLens` — one reference-count lens per top-level
81/// definition in `path`, as `(def site, reference sites)`. The count is
82/// `refs.len()`; the reference sites feed the `showReferences` action. Sorted
83/// by definition position (a stable, top-to-bottom lens order).
84pub fn code_lenses<'a>(index: &'a ProjectIndex, path: &Path) -> Vec<(&'a SiteRef, &'a [SiteRef])> {
85    let mut out: Vec<(&SiteRef, &[SiteRef])> = index
86        .symbols
87        .values()
88        .filter_map(|e| {
89            let def = e.def.as_ref()?;
90            (def.path == path).then_some((def, e.refs.as_slice()))
91        })
92        .collect();
93    out.sort_by_key(|(def, _)| (def.span.start, def.span.end));
94    out
95}
96
97/// v0.34 (ADR 0067): one end of a call-hierarchy relation — the related
98/// symbol (`key` + its definition site) and the call sites linking it to the
99/// queried symbol. For incoming calls `key` is a caller and `sites` are where
100/// it calls the queried symbol; for outgoing, `key` is a callee and `sites`
101/// are where the queried symbol calls it. The sites double as the LSP
102/// `fromRanges` (identical for both directions).
103pub struct CallRelation<'a> {
104    pub key: &'a SymbolKey,
105    pub def: &'a SiteRef,
106    pub sites: Vec<&'a SiteRef>,
107}
108
109/// v0.34 (ADR 0067): `prepareCallHierarchy` — the symbol under the cursor and
110/// its definition site (the goto-def resolution; an item is anchored on the
111/// definition). `None` for out-of-scope positions.
112pub fn prepare_call_hierarchy<'a>(
113    index: &'a ProjectIndex,
114    path: &Path,
115    offset: usize,
116) -> Option<(&'a SymbolKey, &'a SiteRef)> {
117    definition_at(index, path, offset)
118}
119
120/// Group `edges` by the key returned by `pick`, attach each grouped symbol's
121/// definition, and collect the call sites — the shared core of incoming and
122/// outgoing calls. Groups with no indexed definition are dropped (defensive;
123/// every call-edge endpoint is an index symbol by construction). Groups are
124/// ordered by definition position for a stable, top-to-bottom listing.
125fn group_calls<'a>(
126    index: &'a ProjectIndex,
127    edges: impl Iterator<Item = &'a bynk_check::index::CallEdge>,
128    pick: impl Fn(&'a bynk_check::index::CallEdge) -> &'a SymbolKey,
129) -> Vec<CallRelation<'a>> {
130    let mut by_key: BTreeMap<&SymbolKey, Vec<&SiteRef>> = BTreeMap::new();
131    for edge in edges {
132        by_key.entry(pick(edge)).or_default().push(&edge.site);
133    }
134    let mut out: Vec<CallRelation<'a>> = by_key
135        .into_iter()
136        .filter_map(|(key, mut sites)| {
137            let def = index.symbols.get(key)?.def.as_ref()?;
138            sites.sort();
139            Some(CallRelation { key, def, sites })
140        })
141        .collect();
142    out.sort_by_key(|r| (r.def.path.clone(), r.def.span.start, r.def.span.end));
143    out
144}
145
146/// v0.34 (ADR 0067): `callHierarchy/incomingCalls` — the callers of `key`,
147/// each with the call sites at which it calls `key`.
148pub fn incoming_calls<'a>(index: &'a ProjectIndex, key: &SymbolKey) -> Vec<CallRelation<'a>> {
149    group_calls(index, index.calls_into(key), |e| &e.caller)
150}
151
152/// v0.34 (ADR 0067): `callHierarchy/outgoingCalls` — what `key` calls, each
153/// with the call sites within `key` at which the callee is called.
154pub fn outgoing_calls<'a>(index: &'a ProjectIndex, key: &SymbolKey) -> Vec<CallRelation<'a>> {
155    group_calls(index, index.calls_from(key), |e| &e.callee)
156}
157
158/// v0.35 (ADR 0068): `textDocument/implementation` — the definition sites of
159/// every provider implementing the capability `key`, sorted by definition
160/// position. Empty for a non-capability or unknown key (the request then falls
161/// through; goto-def still serves the reverse, provider → capability).
162pub fn implementations<'a>(index: &'a ProjectIndex, key: &SymbolKey) -> Vec<&'a SiteRef> {
163    let mut defs: Vec<&SiteRef> = index
164        .impls_of(key)
165        .filter_map(|e| index.symbols.get(&e.provider)?.def.as_ref())
166        .collect();
167    defs.sort_by_key(|d| (d.path.clone(), d.span.start, d.span.end));
168    defs.dedup();
169    defs
170}
171
172/// Slice 6: `textDocument/typeDefinition` — the definition site(s) of the type
173/// named `name` (a `Type` symbol). The checker's `Ty::Named.name` and the index
174/// both use bare names, so this is a bare-name match; a name shared across units
175/// yields several locations (the LSP-conventional resolution — the client lets
176/// the user choose). Sorted by definition position.
177pub fn type_definitions_named<'a>(index: &'a ProjectIndex, name: &str) -> Vec<&'a SiteRef> {
178    let mut defs: Vec<&SiteRef> = index
179        .symbols
180        .iter()
181        .filter(|(k, _)| k.kind == SymbolKind::Type && k.name == name)
182        .filter_map(|(_, e)| e.def.as_ref())
183        .collect();
184    defs.sort_by_key(|d| (d.path.clone(), d.span.start, d.span.end));
185    defs.dedup();
186    defs
187}
188
189/// The user-declared type a value's type points at, for go-to-type-definition:
190/// a `Named` directly, or the element of a single-parameter container
191/// (`Option`/`Effect`/`List`/`HttpResult`) unwrapped to it. Built-in, function,
192/// actor, and two-parameter (`Result`/`Map`) types have no single
193/// type-declaration target and yield `None`.
194pub fn named_type_target(ty: &Ty) -> Option<&str> {
195    match ty {
196        Ty::Named { name, .. } => Some(name),
197        Ty::Option(t) | Ty::Effect(t) | Ty::List(t) | Ty::HttpResult(t) => named_type_target(t),
198        _ => None,
199    }
200}
201
202/// v0.26 rider (ADR 0055): `documentHighlight` — the symbol-at-cursor's
203/// occurrences within that same file (the `references` query, file-scoped).
204/// The index does not distinguish read from write references, so the LSP
205/// layer omits the highlight `kind`.
206pub fn document_highlights<'a>(
207    index: &'a ProjectIndex,
208    path: &Path,
209    offset: usize,
210) -> Option<Vec<&'a SiteRef>> {
211    let sites = sites_for(index, path, offset, true)?;
212    Some(sites.into_iter().filter(|s| s.path == path).collect())
213}
214
215/// A planned rename: every name-segment edit, grouped per file, spans
216/// ascending. The definition site is edited along with every reference.
217#[derive(Debug, Clone)]
218pub struct RenamePlan {
219    pub key: SymbolKey,
220    pub new_name: String,
221    pub edits: BTreeMap<PathBuf, Vec<Span>>,
222}
223
224/// Build the rename plan for the symbol at the cursor. Errors are
225/// human-readable strings surfaced as LSP request failures.
226pub fn plan_rename(
227    index: &ProjectIndex,
228    path: &Path,
229    offset: usize,
230    new_name: &str,
231) -> Result<RenamePlan, String> {
232    validate_new_name(new_name)?;
233    let (key, _) = index.symbol_at(path, offset).ok_or_else(|| {
234        "no renameable symbol at the cursor — types, fns, methods, record fields, \
235         capability ops, capabilities, services, agents and providers rename; \
236         local bindings and unit names are not yet supported"
237            .to_string()
238    })?;
239    if key_segment(&key.name) == new_name {
240        return Err(format!("`{new_name}` is already the symbol's name"));
241    }
242    let mut edits: BTreeMap<PathBuf, Vec<Span>> = BTreeMap::new();
243    for site in index.sites(key) {
244        edits.entry(site.path.clone()).or_default().push(site.span);
245    }
246    for spans in edits.values_mut() {
247        spans.sort();
248        spans.dedup();
249    }
250    Ok(RenamePlan {
251        key: key.clone(),
252        new_name: new_name.to_string(),
253        edits,
254    })
255}
256
257/// A new name must lex as exactly one identifier (keywords lex as their own
258/// token kinds, so they fail this check).
259pub fn validate_new_name(name: &str) -> Result<(), String> {
260    let err = || format!("`{name}` is not a valid Bynk identifier");
261    let tokens = bynk_syntax::lexer::tokenize(name).map_err(|_| err())?;
262    match tokens.as_slice() {
263        [t] if matches!(t.kind, bynk_syntax::lexer::TokenKind::Ident)
264            && t.span.start == 0
265            && t.span.end == name.len() =>
266        {
267            Ok(())
268        }
269        _ => Err(err()),
270    }
271}
272
273/// Apply one file's edits (spans ascending) to its snapshot text.
274pub fn apply_edits(text: &str, spans: &[Span], new_name: &str) -> String {
275    let mut out = String::with_capacity(text.len());
276    let mut last = 0;
277    for s in spans {
278        out.push_str(&text[last..s.start]);
279        out.push_str(new_name);
280        last = s.end;
281    }
282    out.push_str(&text[last..]);
283    out
284}
285
286/// The post-edit position of a pre-edit site — rename edits shift spans
287/// within edited files. An edited span maps to the new name's span.
288pub fn remap_site(site: &SiteRef, plan: &RenamePlan) -> SiteRef {
289    let Some(spans) = plan.edits.get(&site.path) else {
290        return site.clone();
291    };
292    // The edit replaces the member segment only (`"m"` of `"Type.m"`), so the
293    // length delta is against that segment, not the whole compound key name.
294    let delta = plan.new_name.len() as isize - key_segment(&plan.key.name).len() as isize;
295    let shift: isize = spans.iter().filter(|s| s.end <= site.span.start).count() as isize * delta;
296    let start = (site.span.start as isize + shift) as usize;
297    let end = if spans.binary_search(&site.span).is_ok() {
298        start + plan.new_name.len()
299    } else {
300        (site.span.end as isize + shift) as usize
301    };
302    SiteRef {
303        path: site.path.clone(),
304        span: Span::new(start, end),
305    }
306}
307
308/// Validator (2): the re-built index must equal the pre-index modulo the
309/// rename — every other symbol's reference set identical (after remapping
310/// shifted spans), the renamed symbol's sites exactly the edited ones.
311/// Catches silent re-binding (capture/escape) that produces no diagnostic.
312pub fn index_unchanged_modulo_rename(
313    pre: &ProjectIndex,
314    post: &ProjectIndex,
315    plan: &RenamePlan,
316) -> bool {
317    // v0.36 (ADR 0069): a member key carries a compound name (`"Type.method"`),
318    // but the edit replaces only the member segment — so the post-rename key is
319    // the prefix plus the new segment, not the bare new name.
320    let target = renamed_key_name(&plan.key.name, &plan.new_name);
321    pre.equals_modulo_rename(post, &plan.key, &target, |s| remap_site(s, plan))
322}
323
324/// The post-rename value of a (possibly compound) key name: for a member key
325/// `"Type.method"`, replace the segment after the last `.`; for a bare name,
326/// the new name as-is.
327fn renamed_key_name(old: &str, new_segment: &str) -> String {
328    match old.rfind('.') {
329        Some(i) => format!("{}.{new_segment}", &old[..i]),
330        None => new_segment.to_string(),
331    }
332}
333
334/// The member segment of a (possibly compound) key name — the text the rename
335/// actually edits (every site span covers exactly this).
336fn key_segment(name: &str) -> &str {
337    name.rsplit('.').next().unwrap_or(name)
338}
339
340/// Validator (1): refuse when the edited project carries a diagnostic the
341/// original did not — compared as per-(file, category) counts, robust to
342/// span shifts. Removals are tolerated; additions refuse.
343pub fn no_new_diagnostics(
344    pre: &[(PathBuf, String)],
345    post: &[(PathBuf, String)],
346) -> Result<(), String> {
347    let mut budget: HashMap<(&Path, &str), isize> = HashMap::new();
348    for (p, c) in pre {
349        *budget.entry((p.as_path(), c.as_str())).or_default() += 1;
350    }
351    for (p, c) in post {
352        let n = budget.entry((p.as_path(), c.as_str())).or_default();
353        *n -= 1;
354        if *n < 0 {
355            return Err(format!(
356                "rename would introduce `{c}` in {} — refused",
357                p.display()
358            ));
359        }
360    }
361    Ok(())
362}
363
364// -- v0.28 (ADR 0057): semantic tokens --
365
366/// The frozen semantic-tokens legend. **Array order is the wire encoding**
367/// (clients index into these arrays): entries are append-only, never
368/// reordered — pinned by the legend-stability test. Token types: standard
369/// where faithful (`type`, `function`), custom for the Bynk-distinctive
370/// kinds (`capability`, `service`, `agent`, `provider`).
371pub fn semantic_tokens_legend() -> tower_lsp::lsp_types::SemanticTokensLegend {
372    use tower_lsp::lsp_types::{SemanticTokenModifier, SemanticTokenType, SemanticTokensLegend};
373    SemanticTokensLegend {
374        token_types: vec![
375            SemanticTokenType::TYPE,
376            SemanticTokenType::FUNCTION,
377            SemanticTokenType::new("capability"),
378            SemanticTokenType::new("service"),
379            SemanticTokenType::new("agent"),
380            SemanticTokenType::new("provider"),
381            // v0.31 (ADR 0064): local bindings + params. Standard LSP type —
382            // VS Code themes it by default, no extension declaration needed.
383            SemanticTokenType::VARIABLE,
384            // v0.36 (ADR 0069): instance methods. Appended (never reordered) so
385            // existing legend indices are unchanged. Standard LSP type.
386            SemanticTokenType::METHOD,
387            // v0.36 (ADR 0069, slice 2): record fields. Appended. Standard LSP
388            // type. (Capability ops reuse `method` — they're operation calls.)
389            SemanticTokenType::PROPERTY,
390            // v0.45: actor declarations. Appended at index 9 (never reordered).
391            // Custom type — the VS Code extension declares it in package.json.
392            SemanticTokenType::new("actor"),
393        ],
394        token_modifiers: vec![
395            SemanticTokenModifier::DECLARATION,
396            SemanticTokenModifier::new("refined"),
397            SemanticTokenModifier::new("opaque"),
398            SemanticTokenModifier::new("platformNative"),
399        ],
400    }
401}
402
403/// Legend indices/bits — must mirror [`semantic_tokens_legend`]'s order.
404fn token_type_index(kind: SymbolKind) -> u32 {
405    match kind {
406        SymbolKind::Type => 0,
407        SymbolKind::Fn => 1,
408        SymbolKind::Capability => 2,
409        SymbolKind::Service => 3,
410        SymbolKind::Agent => 4,
411        SymbolKind::Provider => 5,
412        // 6 is `variable` (locals; TOK_LOCAL); methods append at 7.
413        SymbolKind::Method => 7,
414        // v0.36 slice 2: ops reuse `method` (7); fields append `property` at 8.
415        SymbolKind::CapabilityOp => 7,
416        SymbolKind::Field => 8,
417        // v0.45: actors append `actor` at 9.
418        SymbolKind::Actor => 9,
419    }
420}
421
422/// Legend index of the `variable` token type (locals; ADR 0064).
423const TOK_LOCAL: u32 = 6;
424
425const MOD_DECLARATION: u32 = 1 << 0;
426const MOD_REFINED: u32 = 1 << 1;
427const MOD_OPAQUE: u32 = 1 << 2;
428const MOD_PLATFORM_NATIVE: u32 = 1 << 3;
429
430fn modifier_bits(m: bynk_check::index::SymbolModifiers) -> u32 {
431    (if m.refined { MOD_REFINED } else { 0 })
432        | (if m.opaque { MOD_OPAQUE } else { 0 })
433        | (if m.platform_native {
434            MOD_PLATFORM_NATIVE
435        } else {
436            0
437        })
438}
439
440/// Semantic tokens for `path`, delta-encoded over the frozen legend —
441/// a pure read of the cached index's two sources: `symbols` (user
442/// defs+refs; a def site carries `declaration`) and `foreign_refs`
443/// (first-party references). `range` (byte offsets into `text`, the
444/// analysed snapshot) filters to overlapping tokens for the `…/range`
445/// request; `None` is the full document.
446pub fn semantic_tokens(
447    index: &ProjectIndex,
448    local_tokens: &[(Span, bool)],
449    path: &Path,
450    text: &str,
451    range: Option<Span>,
452) -> Vec<tower_lsp::lsp_types::SemanticToken> {
453    let in_scope = |span: Span| {
454        span.end <= text.len() && range.is_none_or(|r| span.end > r.start && span.start < r.end)
455    };
456    let mut raw: Vec<(Span, u32, u32)> = Vec::new();
457    for (key, entry) in &index.symbols {
458        let ty = token_type_index(key.kind);
459        let mods = modifier_bits(entry.modifiers);
460        if let Some(def) = &entry.def
461            && def.path == path
462            && in_scope(def.span)
463        {
464            raw.push((def.span, ty, mods | MOD_DECLARATION));
465        }
466        for site in &entry.refs {
467            if site.path == path && in_scope(site.span) {
468                raw.push((site.span, ty, mods));
469            }
470        }
471    }
472    for fr in &index.foreign_refs {
473        if fr.site.path == path && in_scope(fr.site.span) {
474            raw.push((
475                fr.site.span,
476                token_type_index(fr.kind),
477                modifier_bits(fr.modifiers),
478            ));
479        }
480    }
481    // v0.31 (ADR 0064): local bindings + their uses (precomputed by the caller
482    // via `locals_nav`, so this stays free of that dependency for the
483    // `#[path]`-include tests). Disjoint from the index tokens — locals are
484    // never top-level symbols — so they merge into the same sorted stream.
485    for &(span, is_decl) in local_tokens {
486        if in_scope(span) {
487            raw.push((span, TOK_LOCAL, if is_decl { MOD_DECLARATION } else { 0 }));
488        }
489    }
490    // Name segments never overlap (the index invariant), so a position
491    // sort fully determines the protocol's relative encoding.
492    raw.sort_by_key(|(span, _, _)| (span.start, span.end));
493    let mut data = Vec::with_capacity(raw.len());
494    let (mut prev_line, mut prev_start) = (0u32, 0u32);
495    for (span, token_type, token_modifiers_bitset) in raw {
496        let pos = crate::position::offset_to_position(text, span.start);
497        let delta_line = pos.line - prev_line;
498        let delta_start = if delta_line == 0 {
499            pos.character - prev_start
500        } else {
501            pos.character
502        };
503        data.push(tower_lsp::lsp_types::SemanticToken {
504            delta_line,
505            delta_start,
506            // The protocol counts in the negotiated encoding (UTF-16, as
507            // positions are) — not bytes.
508            length: text[span.range()].encode_utf16().count() as u32,
509            token_type,
510            token_modifiers_bitset,
511        });
512        prev_line = pos.line;
513        prev_start = pos.character;
514    }
515    data
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use bynk_check::index::{SymbolEntry, SymbolKind};
522
523    fn site(path: &str, start: usize, end: usize) -> SiteRef {
524        SiteRef {
525            path: PathBuf::from(path),
526            span: Span::new(start, end),
527        }
528    }
529
530    fn key(unit: &str, kind: SymbolKind, name: &str) -> SymbolKey {
531        SymbolKey {
532            unit: unit.into(),
533            kind,
534            name: name.into(),
535        }
536    }
537
538    fn index_with(entries: Vec<(SymbolKey, SiteRef, Vec<SiteRef>)>) -> ProjectIndex {
539        let mut index = ProjectIndex::default();
540        for (k, def, refs) in entries {
541            index.symbols.insert(
542                k,
543                SymbolEntry {
544                    def: Some(def),
545                    refs,
546                    ..Default::default()
547                },
548            );
549        }
550        index
551    }
552
553    #[test]
554    fn new_name_validation() {
555        assert!(validate_new_name("Money2").is_ok());
556        assert!(validate_new_name("snake_case").is_ok());
557        assert!(validate_new_name("fn").is_err(), "keyword");
558        assert!(validate_new_name("two words").is_err());
559        assert!(validate_new_name("1abc").is_err());
560        assert!(validate_new_name("a.b").is_err());
561        assert!(validate_new_name("").is_err());
562    }
563
564    #[test]
565    fn apply_and_remap_agree() {
566        // text: "fn helper(x: Int) -> Int { helper(x) }"
567        //        3..9 def                  27..33 ref
568        let text = "fn helper(x: Int) -> Int { helper(x) }";
569        let k = key("demo.a", SymbolKind::Fn, "helper");
570        let index = index_with(vec![(
571            k.clone(),
572            site("a.bynk", 3, 9),
573            vec![site("a.bynk", 27, 33)],
574        )]);
575        let plan = plan_rename(&index, Path::new("a.bynk"), 4, "do_it").unwrap();
576        let edited = apply_edits(text, &plan.edits[Path::new("a.bynk")], "do_it");
577        assert_eq!(edited, "fn do_it(x: Int) -> Int { do_it(x) }");
578
579        // Remap maps both old sites onto the new spellings.
580        for (old, expected) in [
581            (site("a.bynk", 3, 9), "do_it"),
582            (site("a.bynk", 27, 33), "do_it"),
583        ] {
584            let new = remap_site(&old, &plan);
585            assert_eq!(&edited[new.span.range()], expected);
586        }
587        // An unedited later site shifts by the accumulated delta.
588        let unrelated = site("a.bynk", 34, 35); // `x` argument
589        let new = remap_site(&unrelated, &plan);
590        assert_eq!(&edited[new.span.range()], "x");
591    }
592
593    #[test]
594    fn references_listing_orders_definition_first() {
595        let k = key("demo.a", SymbolKind::Type, "Money");
596        let index = index_with(vec![(
597            k,
598            site("a.bynk", 5, 10),
599            vec![site("b.bynk", 1, 6), site("a.bynk", 20, 25)],
600        )]);
601        let all = sites_for(&index, Path::new("b.bynk"), 3, true).unwrap();
602        assert_eq!(all.len(), 3);
603        assert_eq!(all[0].path, PathBuf::from("a.bynk"));
604        assert_eq!(all[0].span, Span::new(5, 10));
605        let without_decl = sites_for(&index, Path::new("b.bynk"), 3, false).unwrap();
606        assert_eq!(without_decl.len(), 2);
607    }
608
609    #[test]
610    fn rename_refuses_unindexed_positions_and_same_name() {
611        let k = key("demo.a", SymbolKind::Fn, "helper");
612        let index = index_with(vec![(k, site("a.bynk", 3, 9), vec![])]);
613        assert!(plan_rename(&index, Path::new("a.bynk"), 100, "x").is_err());
614        assert!(plan_rename(&index, Path::new("a.bynk"), 4, "helper").is_err());
615    }
616
617    #[test]
618    fn index_equality_detects_escape() {
619        // Pre: `helper` has no references; some other symbol unchanged.
620        let helper = key("demo.a", SymbolKind::Fn, "helper");
621        let money = key("demo.a", SymbolKind::Type, "Money");
622        let pre = index_with(vec![
623            (helper.clone(), site("a.bynk", 3, 9), vec![]),
624            (money.clone(), site("a.bynk", 50, 55), vec![]),
625        ]);
626        let plan = plan_rename(&pre, Path::new("a.bynk"), 4, "shadow").unwrap();
627
628        // Post (honest): def renamed in place, still no refs.
629        let honest = index_with(vec![
630            (
631                key("demo.a", SymbolKind::Fn, "shadow"),
632                site("a.bynk", 3, 9),
633                vec![],
634            ),
635            (money.clone(), site("a.bynk", 50, 55), vec![]),
636        ]);
637        assert!(index_unchanged_modulo_rename(&pre, &honest, &plan));
638
639        // Post (escape): a site that used to bind elsewhere now resolves to
640        // the renamed symbol — an extra reference appears.
641        let escape = index_with(vec![
642            (
643                key("demo.a", SymbolKind::Fn, "shadow"),
644                site("a.bynk", 3, 9),
645                vec![site("a.bynk", 70, 76)],
646            ),
647            (money, site("a.bynk", 50, 55), vec![]),
648        ]);
649        assert!(!index_unchanged_modulo_rename(&pre, &escape, &plan));
650    }
651
652    #[test]
653    fn method_rename_edits_the_member_segment_and_remaps_the_compound_key() {
654        // v0.36: a method key is compound (`"Counter.bump"`), but the edit
655        // touches only the `bump` segment — so the plan's new name is the bare
656        // segment, the post key is `"Counter.increment"`, and the span delta is
657        // against the segment length (4), not the compound length (12).
658        let bump = key("demo.a", SymbolKind::Method, "Counter.bump");
659        let other = key("demo.a", SymbolKind::Type, "Counter");
660        let pre = index_with(vec![
661            (other.clone(), site("a.bynk", 0, 5), vec![]),
662            // def `bump` at 11..15, one call at 40..44.
663            (
664                bump.clone(),
665                site("a.bynk", 11, 15),
666                vec![site("a.bynk", 40, 44)],
667            ),
668        ]);
669
670        // Cursor on the def segment; rename to a longer name.
671        let plan = plan_rename(&pre, Path::new("a.bynk"), 12, "increment").unwrap();
672        assert_eq!(plan.key.name, "Counter.bump");
673        assert_eq!(plan.new_name, "increment");
674        // Renaming to the same segment is refused (segment-aware, not key-aware).
675        assert!(plan_rename(&pre, Path::new("a.bynk"), 12, "bump").is_err());
676
677        // Honest post: the compound key becomes `Counter.increment`; the def
678        // grows in place (11..20) and the call shifts by +5 (45..54).
679        let post = index_with(vec![
680            (other, site("a.bynk", 0, 5), vec![]),
681            (
682                key("demo.a", SymbolKind::Method, "Counter.increment"),
683                site("a.bynk", 11, 20),
684                vec![site("a.bynk", 45, 54)],
685            ),
686        ]);
687        assert!(
688            index_unchanged_modulo_rename(&pre, &post, &plan),
689            "compound key remaps to Counter.increment and segment-based delta lines the spans up"
690        );
691    }
692
693    #[test]
694    fn workspace_symbols_filters_and_orders() {
695        let index = index_with(vec![
696            (
697                key("demo.a", SymbolKind::Type, "Money"),
698                site("a.bynk", 5, 10),
699                vec![],
700            ),
701            (
702                key("demo.b", SymbolKind::Fn, "moneyMaker"),
703                site("b.bynk", 3, 13),
704                vec![],
705            ),
706            (
707                key("demo.a", SymbolKind::Fn, "helper"),
708                site("a.bynk", 40, 46),
709                vec![],
710            ),
711        ]);
712        // Case-insensitive substring match.
713        let hits = workspace_symbols(&index, "money");
714        assert_eq!(
715            hits.iter()
716                .map(|(k, _)| k.name.as_str())
717                .collect::<Vec<_>>(),
718            vec!["Money", "moneyMaker"]
719        );
720        // Empty query lists everything, (name, unit)-ordered.
721        assert_eq!(workspace_symbols(&index, "").len(), 3);
722        assert!(workspace_symbols(&index, "nothing").is_empty());
723    }
724
725    #[test]
726    fn document_highlights_are_file_scoped() {
727        let k = key("demo.a", SymbolKind::Type, "Money");
728        let index = index_with(vec![(
729            k,
730            site("a.bynk", 5, 10),
731            vec![site("b.bynk", 1, 6), site("a.bynk", 20, 25)],
732        )]);
733        // From a.bynk: the definition + the in-file reference, not b.bynk's.
734        let highlights = document_highlights(&index, Path::new("a.bynk"), 7).unwrap();
735        assert_eq!(highlights.len(), 2);
736        assert!(highlights.iter().all(|s| s.path == Path::new("a.bynk")));
737        // No symbol at the cursor → None.
738        assert!(document_highlights(&index, Path::new("a.bynk"), 100).is_none());
739    }
740
741    #[test]
742    fn diagnostic_budget_allows_removals_refuses_additions() {
743        let pre = vec![
744            (PathBuf::from("a.bynk"), "bynk.x".to_string()),
745            (PathBuf::from("a.bynk"), "bynk.x".to_string()),
746        ];
747        let same = pre.clone();
748        assert!(no_new_diagnostics(&pre, &same).is_ok());
749        assert!(no_new_diagnostics(&pre, &pre[..1]).is_ok());
750        let mut more = pre.clone();
751        more.push((PathBuf::from("b.bynk"), "bynk.resolve.duplicate_fn".into()));
752        assert!(no_new_diagnostics(&pre, &more).is_err());
753    }
754
755    // -- v0.28 (ADR 0057): semantic tokens --
756
757    /// The legend's array order IS the wire encoding: this test freezes it.
758    /// New entries APPEND — a failure here means a silent recolour of every
759    /// client; never fix it by reordering.
760    #[test]
761    fn legend_is_frozen() {
762        let legend = semantic_tokens_legend();
763        let types: Vec<&str> = legend.token_types.iter().map(|t| t.as_str()).collect();
764        assert_eq!(
765            types,
766            [
767                "type",
768                "function",
769                "capability",
770                "service",
771                "agent",
772                "provider",
773                "variable", // v0.31 (ADR 0064): locals — appended, never reordered
774                "method",   // v0.36 (ADR 0069): instance methods — appended
775                "property", // v0.36 (ADR 0069, slice 2): record fields — appended
776                "actor",    // v0.45: actor declarations — appended
777            ]
778        );
779        let modifiers: Vec<&str> = legend.token_modifiers.iter().map(|m| m.as_str()).collect();
780        assert_eq!(
781            modifiers,
782            ["declaration", "refined", "opaque", "platformNative"]
783        );
784    }
785
786    #[test]
787    fn code_lenses_count_references_per_definition_in_the_file() {
788        let index = index_with(vec![
789            // `foo` defined in a.bynk with two references.
790            (
791                key("u", SymbolKind::Fn, "foo"),
792                site("a.bynk", 3, 6),
793                vec![site("a.bynk", 20, 23), site("b.bynk", 4, 7)],
794            ),
795            // `Bar` defined in a.bynk with no references (a 0-ref lens).
796            (
797                key("u", SymbolKind::Type, "Bar"),
798                site("a.bynk", 40, 43),
799                vec![],
800            ),
801            // `qux` defined in another file — no lens for a.bynk.
802            (
803                key("u", SymbolKind::Fn, "qux"),
804                site("b.bynk", 0, 3),
805                vec![],
806            ),
807        ]);
808        let lenses = code_lenses(&index, Path::new("a.bynk"));
809        assert_eq!(lenses.len(), 2, "two a.bynk defs get lenses");
810        // Sorted by def position: foo (3..6) before Bar (40..43).
811        assert_eq!((lenses[0].0.span.start, lenses[0].1.len()), (3, 2));
812        assert_eq!((lenses[1].0.span.start, lenses[1].1.len()), (40, 0));
813        assert!(code_lenses(&index, Path::new("c.bynk")).is_empty());
814    }
815
816    #[test]
817    fn call_hierarchy_groups_incoming_and_outgoing_by_symbol() {
818        use bynk_check::index::CallEdge;
819        // `a` and `b` both call `c`; `a` calls `c` twice. So `c`'s incoming
820        // groups by caller (a with two sites, b with one), and `a`'s outgoing
821        // is the single callee `c`.
822        let mut index = index_with(vec![
823            (key("u", SymbolKind::Fn, "a"), site("f.bynk", 3, 4), vec![]),
824            (
825                key("u", SymbolKind::Fn, "b"),
826                site("f.bynk", 40, 41),
827                vec![],
828            ),
829            (
830                key("u", SymbolKind::Fn, "c"),
831                site("f.bynk", 80, 81),
832                vec![],
833            ),
834        ]);
835        let edge = |caller: &str, cs: usize, ce: usize| CallEdge {
836            caller: key("u", SymbolKind::Fn, caller),
837            callee: key("u", SymbolKind::Fn, "c"),
838            site: site("f.bynk", cs, ce),
839        };
840        index.calls = vec![edge("a", 10, 11), edge("a", 20, 21), edge("b", 50, 51)];
841
842        let into_c = incoming_calls(&index, &key("u", SymbolKind::Fn, "c"));
843        // Sorted by caller def position: a (3) before b (40).
844        assert_eq!(into_c.len(), 2);
845        assert_eq!(
846            (into_c[0].key.name.as_str(), into_c[0].sites.len()),
847            ("a", 2)
848        );
849        assert_eq!(
850            (into_c[1].key.name.as_str(), into_c[1].sites.len()),
851            ("b", 1)
852        );
853
854        let from_a = outgoing_calls(&index, &key("u", SymbolKind::Fn, "a"));
855        assert_eq!(from_a.len(), 1);
856        assert_eq!(
857            (from_a[0].key.name.as_str(), from_a[0].sites.len()),
858            ("c", 2)
859        );
860
861        // `c` calls nothing; an unknown key yields nothing.
862        assert!(outgoing_calls(&index, &key("u", SymbolKind::Fn, "c")).is_empty());
863        assert!(incoming_calls(&index, &key("u", SymbolKind::Fn, "ghost")).is_empty());
864    }
865
866    #[test]
867    fn implementations_lists_provider_defs_for_a_capability() {
868        use bynk_check::index::ImplEdge;
869        // `Cap` is provided by `P1` and `P2`; `Other` (a capability) has none.
870        let mut index = index_with(vec![
871            (
872                key("u", SymbolKind::Capability, "Cap"),
873                site("a.bynk", 10, 13),
874                vec![],
875            ),
876            (
877                key("u", SymbolKind::Provider, "P1"),
878                site("a.bynk", 50, 52),
879                vec![],
880            ),
881            (
882                key("u", SymbolKind::Provider, "P2"),
883                site("b.bynk", 5, 7),
884                vec![],
885            ),
886            (
887                key("u", SymbolKind::Capability, "Other"),
888                site("a.bynk", 80, 85),
889                vec![],
890            ),
891        ]);
892        let edge = |provider: &str, file: &str, s: usize, e: usize| ImplEdge {
893            capability: key("u", SymbolKind::Capability, "Cap"),
894            provider: key("u", SymbolKind::Provider, provider),
895            site: site(file, s, e),
896        };
897        index.impls = vec![edge("P1", "a.bynk", 30, 33), edge("P2", "b.bynk", 20, 23)];
898
899        // Provider defs, sorted by position: P1 (a.bynk:50) before P2 (b.bynk:5).
900        let impls = implementations(&index, &key("u", SymbolKind::Capability, "Cap"));
901        assert_eq!(impls.len(), 2);
902        assert_eq!(
903            (&impls[0].path, impls[0].span.start),
904            (&PathBuf::from("a.bynk"), 50)
905        );
906        assert_eq!(
907            (&impls[1].path, impls[1].span.start),
908            (&PathBuf::from("b.bynk"), 5)
909        );
910
911        // A capability with no providers, and an unknown key, yield nothing.
912        assert!(implementations(&index, &key("u", SymbolKind::Capability, "Other")).is_empty());
913        assert!(implementations(&index, &key("u", SymbolKind::Capability, "Ghost")).is_empty());
914    }
915
916    #[test]
917    fn type_definitions_named_collects_type_defs_by_bare_name() {
918        // Two units each declare an `Order` type; a fn shares the name.
919        let index = index_with(vec![
920            (
921                key("a", SymbolKind::Type, "Order"),
922                site("a.bynk", 10, 15),
923                vec![],
924            ),
925            (
926                key("b", SymbolKind::Type, "Order"),
927                site("b.bynk", 4, 9),
928                vec![],
929            ),
930            (
931                key("a", SymbolKind::Fn, "Order"),
932                site("a.bynk", 40, 45),
933                vec![],
934            ),
935        ]);
936        // Both `Type` defs (not the fn), sorted by position.
937        let defs = type_definitions_named(&index, "Order");
938        assert_eq!(defs.len(), 2);
939        assert_eq!(
940            (&defs[0].path, defs[0].span.start),
941            (&PathBuf::from("a.bynk"), 10)
942        );
943        assert_eq!(
944            (&defs[1].path, defs[1].span.start),
945            (&PathBuf::from("b.bynk"), 4)
946        );
947        // An unknown type name yields nothing.
948        assert!(type_definitions_named(&index, "Nope").is_empty());
949    }
950
951    #[test]
952    fn named_type_target_unwraps_single_param_containers() {
953        use bynk_check::checker::NamedKind;
954        use bynk_syntax::ast::BaseType;
955        let order = || Ty::Named {
956            name: "Order".into(),
957            kind: NamedKind::Record,
958        };
959        assert_eq!(named_type_target(&order()), Some("Order"));
960        assert_eq!(
961            named_type_target(&Ty::Option(Box::new(order()))),
962            Some("Order")
963        );
964        // Nested single-param containers unwrap all the way.
965        assert_eq!(
966            named_type_target(&Ty::List(Box::new(Ty::Effect(Box::new(order()))))),
967            Some("Order")
968        );
969        // Built-in, two-parameter, and unit types have no single target.
970        assert_eq!(named_type_target(&Ty::Base(BaseType::Int)), None);
971        assert_eq!(
972            named_type_target(&Ty::Result(Box::new(order()), Box::new(order()))),
973            None
974        );
975        assert_eq!(named_type_target(&Ty::Unit), None);
976    }
977
978    #[test]
979    fn tokens_are_delta_encoded_with_modifier_bitsets() {
980        // text:  line 0: "type Age = Int"   (def `Age` at 5..8, refined)
981        //        line 1: "fn f(a: Age) ..." (ref `Age` at 23..26)
982        let text = "type Age = Int\nfn f(a: Age) -> Age {}\n";
983        let mut index = index_with(vec![(
984            key("shop", SymbolKind::Type, "Age"),
985            site("a.bynk", 5, 8),
986            vec![site("a.bynk", 23, 26), site("a.bynk", 31, 34)],
987        )]);
988        index
989            .symbols
990            .get_mut(&key("shop", SymbolKind::Type, "Age"))
991            .unwrap()
992            .modifiers = bynk_check::index::SymbolModifiers {
993            refined: true,
994            ..Default::default()
995        };
996        let tokens = semantic_tokens(&index, &[], Path::new("a.bynk"), text, None);
997        assert_eq!(tokens.len(), 3);
998        // Def: line 0 char 5, length 3, type `type` (0), declaration|refined.
999        assert_eq!(
1000            (
1001                tokens[0].delta_line,
1002                tokens[0].delta_start,
1003                tokens[0].length
1004            ),
1005            (0, 5, 3)
1006        );
1007        assert_eq!(tokens[0].token_type, 0);
1008        assert_eq!(tokens[0].token_modifiers_bitset, 0b0011);
1009        // First ref: next line, char 8 (absolute — line changed), refined only.
1010        assert_eq!(
1011            (
1012                tokens[1].delta_line,
1013                tokens[1].delta_start,
1014                tokens[1].length
1015            ),
1016            (1, 8, 3)
1017        );
1018        assert_eq!(tokens[1].token_modifiers_bitset, 0b0010);
1019        // Second ref: same line, char delta from the previous token's start.
1020        assert_eq!(
1021            (
1022                tokens[2].delta_line,
1023                tokens[2].delta_start,
1024                tokens[2].length
1025            ),
1026            (0, 8, 3)
1027        );
1028    }
1029
1030    #[test]
1031    fn foreign_refs_emit_tokens_and_range_filters() {
1032        let text = "given Kv {\n  Kv.get(k)\n}\n";
1033        let mut index = ProjectIndex::default();
1034        index.foreign_refs.push(bynk_check::index::ForeignRef {
1035            site: site("a.bynk", 6, 8),
1036            kind: SymbolKind::Capability,
1037            modifiers: bynk_check::index::SymbolModifiers {
1038                platform_native: true,
1039                ..Default::default()
1040            },
1041        });
1042        index.foreign_refs.push(bynk_check::index::ForeignRef {
1043            site: site("a.bynk", 13, 15),
1044            kind: SymbolKind::Capability,
1045            modifiers: bynk_check::index::SymbolModifiers {
1046                platform_native: true,
1047                ..Default::default()
1048            },
1049        });
1050        let all = semantic_tokens(&index, &[], Path::new("a.bynk"), text, None);
1051        assert_eq!(all.len(), 2);
1052        assert_eq!(all[0].token_type, 2); // capability
1053        assert_eq!(all[0].token_modifiers_bitset, 0b1000); // platformNative
1054        // Range covering only line 0 keeps only the first token.
1055        let ranged = semantic_tokens(
1056            &index,
1057            &[],
1058            Path::new("a.bynk"),
1059            text,
1060            Some(Span::new(0, 10)),
1061        );
1062        assert_eq!(ranged.len(), 1);
1063        // Other files and empty indexes yield nothing.
1064        assert!(semantic_tokens(&index, &[], Path::new("b.bynk"), text, None).is_empty());
1065        assert!(
1066            semantic_tokens(
1067                &ProjectIndex::default(),
1068                &[],
1069                Path::new("a.bynk"),
1070                text,
1071                None
1072            )
1073            .is_empty()
1074        );
1075    }
1076}