Skip to main content

drawlang_syntax/
span.rs

1//! Byte-offset spans and source-file line/column mapping.
2
3use serde::Serialize;
4
5/// A half-open byte range into a source file.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
7pub struct Span {
8    pub start: usize,
9    pub end: usize,
10}
11
12impl Span {
13    pub fn new(start: usize, end: usize) -> Self {
14        Span { start, end }
15    }
16
17    pub const DUMMY: Span = Span { start: 0, end: 0 };
18
19    /// Smallest span covering both `self` and `other`.
20    pub fn to(self, other: Span) -> Span {
21        Span::new(self.start.min(other.start), self.end.max(other.end))
22    }
23
24    pub fn len(&self) -> usize {
25        self.end.saturating_sub(self.start)
26    }
27
28    pub fn is_empty(&self) -> bool {
29        self.len() == 0
30    }
31}
32
33/// 1-based line/column position.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
35pub struct LineCol {
36    pub line: usize,
37    pub col: usize,
38}
39
40/// A source file plus the precomputed line table needed to resolve spans.
41#[derive(Debug, Clone)]
42pub struct SourceFile {
43    pub name: String,
44    pub text: String,
45    line_starts: Vec<usize>,
46}
47
48impl SourceFile {
49    pub fn new(name: impl Into<String>, text: impl Into<String>) -> Self {
50        let text = text.into();
51        let mut line_starts = vec![0];
52        for (i, b) in text.bytes().enumerate() {
53            if b == b'\n' {
54                line_starts.push(i + 1);
55            }
56        }
57        SourceFile {
58            name: name.into(),
59            text,
60            line_starts,
61        }
62    }
63
64    /// Line/column (1-based) for a byte offset. Columns count Unicode scalar
65    /// values, not bytes, so carets line up with what an editor shows.
66    pub fn line_col(&self, offset: usize) -> LineCol {
67        let offset = offset.min(self.text.len());
68        let line_idx = match self.line_starts.binary_search(&offset) {
69            Ok(i) => i,
70            Err(i) => i - 1,
71        };
72        let line_start = self.line_starts[line_idx];
73        let col = self.text[line_start..offset].chars().count() + 1;
74        LineCol {
75            line: line_idx + 1,
76            col,
77        }
78    }
79
80    /// The full text of a 1-based line, without its trailing newline.
81    pub fn line_text(&self, line: usize) -> &str {
82        let idx = line - 1;
83        let start = self.line_starts[idx];
84        let end = self
85            .line_starts
86            .get(idx + 1)
87            .copied()
88            .unwrap_or(self.text.len());
89        self.text[start..end].trim_end_matches(['\n', '\r'])
90    }
91
92    pub fn line_count(&self) -> usize {
93        self.line_starts.len()
94    }
95
96    pub fn snippet(&self, span: Span) -> &str {
97        &self.text[span.start.min(self.text.len())..span.end.min(self.text.len())]
98    }
99}