Skip to main content

bbnf_analysis/
analysis.rs

1use ls_types::*;
2
3use crate::state::{DocumentInfo, RuleInfo};
4
5// ---------------------------------------------------------------------------
6// LineIndex — O(1) build, O(log n) lookup
7// ---------------------------------------------------------------------------
8
9/// Precomputed line-start offsets for O(log n) position conversion.
10#[derive(Debug, Clone)]
11pub struct LineIndex {
12    /// Byte offset of the start of each line. `line_starts[0]` is always 0.
13    line_starts: Vec<usize>,
14    /// Total text length in bytes.
15    text_len: usize,
16}
17
18impl LineIndex {
19    /// Build a line index from document text. O(n) in text length.
20    pub fn new(text: &str) -> Self {
21        let mut line_starts = vec![0];
22        for (i, byte) in text.bytes().enumerate() {
23            if byte == b'\n' {
24                line_starts.push(i + 1);
25            }
26        }
27        Self {
28            line_starts,
29            text_len: text.len(),
30        }
31    }
32
33    /// Convert a byte offset to an LSP Position. O(log n) via binary search.
34    pub fn offset_to_position(&self, offset: usize) -> Position {
35        let line = self.line_starts.partition_point(|&s| s <= offset).saturating_sub(1);
36        let col = offset.saturating_sub(self.line_starts[line]);
37        Position::new(line as u32, col as u32)
38    }
39
40    /// Convert an LSP Position to a byte offset. O(1).
41    pub fn position_to_offset(&self, pos: Position) -> usize {
42        let line = pos.line as usize;
43        if line < self.line_starts.len() {
44            let line_start = self.line_starts[line];
45            let line_end = if line + 1 < self.line_starts.len() {
46                // Clamp to the '\n' byte for non-last lines.
47                self.line_starts[line + 1].saturating_sub(1)
48            } else {
49                // Last line can clamp to EOF.
50                self.text_len
51            };
52            let requested = line_start + pos.character as usize;
53            requested.min(line_end)
54        } else {
55            panic!(
56                "position_to_offset received out-of-range line {} (line_count={})",
57                line,
58                self.line_starts.len()
59            );
60        }
61    }
62
63    /// Convert byte offset span to an LSP Range.
64    pub fn span_to_range(&self, start: usize, end: usize) -> Range {
65        Range::new(self.offset_to_position(start), self.offset_to_position(end))
66    }
67}
68
69// ---------------------------------------------------------------------------
70// Symbol lookup
71// ---------------------------------------------------------------------------
72
73/// What lives at a given byte offset?
74#[derive(Debug)]
75pub enum SymbolAtOffset<'a> {
76    /// Cursor is on the LHS definition of a rule.
77    RuleDefinition(&'a RuleInfo),
78    /// Cursor is on a nonterminal reference in some rule's RHS.
79    RuleReference {
80        /// Name of the referenced nonterminal.
81        name: String,
82        /// Byte span of this reference token.
83        span: (usize, usize),
84    },
85}
86
87/// Resolve what symbol is at the given byte offset.
88pub fn symbol_at_offset<'a>(info: &'a DocumentInfo, offset: usize) -> Option<SymbolAtOffset<'a>> {
89    // Check rule definitions first.
90    for rule in &info.rules {
91        if offset >= rule.name_span.0 && offset <= rule.name_span.1 {
92            return Some(SymbolAtOffset::RuleDefinition(rule));
93        }
94    }
95    // Check references in all rules.
96    for rule in &info.rules {
97        for refinfo in &rule.references {
98            if offset >= refinfo.span.0 && offset <= refinfo.span.1 {
99                return Some(SymbolAtOffset::RuleReference {
100                    name: refinfo.name.clone(),
101                    span: refinfo.span,
102                });
103            }
104        }
105    }
106    // Check @recover directive rule names.
107    for rec in &info.recovers {
108        if offset >= rec.rule_name_span.0 && offset <= rec.rule_name_span.1 {
109            return Some(SymbolAtOffset::RuleReference {
110                name: rec.rule_name.clone(),
111                span: rec.rule_name_span,
112            });
113        }
114    }
115    // Check @no_collapse directive rule names.
116    for nc in &info.no_collapses {
117        if offset >= nc.rule_name_span.0 && offset <= nc.rule_name_span.1 {
118            return Some(SymbolAtOffset::RuleReference {
119                name: nc.rule_name.clone(),
120                span: nc.rule_name_span,
121            });
122        }
123    }
124    // Check @pretty directive rule names.
125    for pretty in &info.pretties {
126        if offset >= pretty.rule_name_span.0 && offset <= pretty.rule_name_span.1 {
127            return Some(SymbolAtOffset::RuleReference {
128                name: pretty.rule_name.clone(),
129                span: pretty.rule_name_span,
130            });
131        }
132    }
133    None
134}