php_parser/
line_index.rs

1use crate::span::Span;
2
3#[derive(Debug, Clone)]
4pub struct LineIndex {
5    /// Offset of the start of each line.
6    line_starts: Vec<usize>,
7    len: usize,
8}
9
10impl LineIndex {
11    pub fn new(source: &[u8]) -> Self {
12        let mut line_starts = vec![0];
13        for (i, &b) in source.iter().enumerate() {
14            if b == b'\n' {
15                line_starts.push(i + 1);
16            }
17        }
18        Self {
19            line_starts,
20            len: source.len(),
21        }
22    }
23
24    /// Returns (line, column) for a given byte offset.
25    /// Both line and column are 0-based.
26    pub fn line_col(&self, offset: usize) -> (usize, usize) {
27        if offset > self.len {
28            // Fallback or panic? For robustness, clamp to end.
29            let last_line = self.line_starts.len() - 1;
30            let last_start = self.line_starts[last_line];
31            return (last_line, self.len.saturating_sub(last_start));
32        }
33
34        // Binary search to find the line
35        match self.line_starts.binary_search(&offset) {
36            Ok(line) => (line, 0),
37            Err(insert_idx) => {
38                let line = insert_idx - 1;
39                let col = offset - self.line_starts[line];
40                (line, col)
41            }
42        }
43    }
44
45    /// Returns the byte offset for a given (line, column).
46    /// Both line and column are 0-based.
47    pub fn offset(&self, line: usize, col: usize) -> Option<usize> {
48        if line >= self.line_starts.len() {
49            return None;
50        }
51        let start = self.line_starts[line];
52        let offset = start + col;
53
54        // Check if offset is within the line (or at least within file bounds)
55        // We don't strictly check if col goes beyond the line length here,
56        // but we should check if it goes beyond the next line start.
57        if line + 1 < self.line_starts.len() && offset >= self.line_starts[line + 1] {
58            // Column is too large for this line
59            // But maybe we allow it if it points to the newline char?
60            // LSP allows pointing past the end of line.
61            // But strictly speaking, it shouldn't cross into the next line.
62            // For now, let's just check total length.
63        }
64
65        if offset > self.len {
66            None
67        } else {
68            Some(offset)
69        }
70    }
71
72    pub fn to_lsp_range(&self, span: Span) -> (usize, usize, usize, usize) {
73        let (start_line, start_col) = self.line_col(span.start);
74        let (end_line, end_col) = self.line_col(span.end);
75        (start_line, start_col, end_line, end_col)
76    }
77}