Skip to main content

lutra_compiler/project/
analysis.rs

1use std::collections::HashMap;
2
3use crate::Span;
4use crate::pr::Path;
5
6use super::Project;
7
8/// Information about a symbol at a given source position.
9///
10/// Returned by [`Project::find_by_span`] and used by LSP features such as
11/// go-to-definition and hover.
12pub struct SymbolInfo {
13    /// Span of the identifier token at the queried position.
14    pub source_span: Span,
15    /// Path of the referenced global definition, or `None`
16    /// for local bindings.
17    pub target_path: Option<Path>,
18    /// Span of the definition site (go-to-definition target).
19    pub target_span: Option<Span>,
20}
21
22impl Project {
23    /// Look up the symbol at `(source_id, offset)` — a byte offset within a
24    /// source file — and return information about what it refers to.
25    ///
26    /// Returns `None` when the offset does not correspond to a known identifier.
27    pub fn find_by_span(&self, source_id: u16, offset: u32) -> Option<SymbolInfo> {
28        let (target, source_span) = self.target_map.find_at(source_id, offset)?;
29        Some(match target {
30            TargetSpan::Global(path) => {
31                let target_span = self.root_module.get(path).and_then(|d| d.span_name);
32                SymbolInfo {
33                    source_span,
34                    target_path: Some(path.clone()),
35                    target_span,
36                }
37            }
38            TargetSpan::Span(target_span) => SymbolInfo {
39                source_span,
40                target_path: None,
41                target_span: Some(*target_span),
42            },
43        })
44    }
45}
46
47/// An index of all resolved identifier references in a compiled project,
48/// keyed by source location.  Built once after name resolution and used for
49/// queries like go-to-definition.
50#[derive(Debug, Default)]
51pub struct TargetMap {
52    /// One sorted vec per source file (keyed by source_id).
53    /// Each vec is sorted by `TargetEntry::start` to allow binary search.
54    by_source: HashMap<u16, Vec<TargetEntry>>,
55}
56
57#[derive(Debug)]
58struct TargetEntry {
59    start: u32,
60    len: u16,
61    target: TargetSpan,
62}
63
64#[derive(Debug)]
65pub enum TargetSpan {
66    Global(Path),
67    Span(Span),
68}
69
70impl TargetMap {
71    /// Build a `TargetMap` from a flat list of `(span, target)` pairs collected
72    /// during name resolution.  Overlay source IDs (`u16::MAX`) are excluded.
73    pub fn build(entries: Vec<(Span, TargetSpan)>) -> Self {
74        let mut by_source: HashMap<u16, Vec<TargetEntry>> = HashMap::new();
75
76        for (span, target) in entries {
77            let entry = by_source.entry(span.source_id).or_default();
78            entry.push(TargetEntry {
79                start: span.start,
80                len: span.len,
81                target,
82            });
83        }
84
85        for vec in by_source.values_mut() {
86            vec.sort_by_key(|e| e.start);
87        }
88
89        TargetMap { by_source }
90    }
91
92    /// Find the resolved target for the identifier at `(source_id, offset)`.
93    ///
94    /// Returns both the [`TargetSpan`] and the source [`Span`] of the matched
95    /// token (i.e. the span of the identifier itself, useful as the LSP hover
96    /// range).
97    ///
98    /// When multiple spans contain the offset (e.g. nested expressions share a
99    /// start position), the innermost one (smallest `len`) is returned.
100    pub fn find_at(&self, source_id: u16, offset: u32) -> Option<(&TargetSpan, Span)> {
101        let entries = self.by_source.get(&source_id)?;
102
103        // All entries with start <= offset are candidates; find the boundary.
104        let end = entries.partition_point(|e| e.start <= offset);
105
106        entries[..end]
107            .iter()
108            .filter(|e| e.start + e.len as u32 > offset)
109            .min_by_key(|e| e.len)
110            .map(|e| {
111                let span = Span {
112                    source_id,
113                    start: e.start,
114                    len: e.len,
115                };
116                (&e.target, span)
117            })
118    }
119}