Skip to main content

nautilus_schema/
span.rs

1//! Source code position and span tracking for diagnostics.
2
3use std::fmt;
4
5/// A position in source code (line and column).
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Position {
8    /// Line number (1-indexed).
9    pub line: usize,
10    /// Column number (1-indexed).
11    pub column: usize,
12}
13
14impl Position {
15    /// Create a new position.
16    pub const fn new(line: usize, column: usize) -> Self {
17        Self { line, column }
18    }
19}
20
21impl fmt::Display for Position {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        write!(f, "{}:{}", self.line, self.column)
24    }
25}
26
27/// A span in source code (byte offsets).
28///
29/// Spans use byte offsets for efficient slicing. Line/column information
30/// can be computed from the source text when needed for diagnostics.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub struct Span {
33    /// Byte offset of the start of the span (inclusive).
34    pub start: usize,
35    /// Byte offset of the end of the span (exclusive).
36    pub end: usize,
37}
38
39impl Span {
40    /// Create a new span.
41    pub const fn new(start: usize, end: usize) -> Self {
42        Self { start, end }
43    }
44
45    /// Create a span from a single byte offset.
46    pub const fn single(pos: usize) -> Self {
47        Self {
48            start: pos,
49            end: pos + 1,
50        }
51    }
52
53    /// Merge two spans into one that covers both.
54    pub const fn merge(self, other: Span) -> Span {
55        let start = if self.start < other.start {
56            self.start
57        } else {
58            other.start
59        };
60        let end = if self.end > other.end {
61            self.end
62        } else {
63            other.end
64        };
65        Span { start, end }
66    }
67
68    /// Get the length of the span in bytes.
69    pub const fn len(&self) -> usize {
70        self.end - self.start
71    }
72
73    /// Check if the span is empty.
74    pub const fn is_empty(&self) -> bool {
75        self.start == self.end
76    }
77
78    /// Extract the text covered by this span from source.
79    pub fn slice<'a>(&self, source: &'a str) -> &'a str {
80        &source[self.start..self.end]
81    }
82
83    /// Convert byte offset span to line/column positions.
84    ///
85    /// This scans the source text to compute line and column numbers.
86    /// For performance, avoid calling this repeatedly; cache results if needed.
87    pub fn to_positions(&self, source: &str) -> (Position, Position) {
88        let start_pos = byte_offset_to_position(source, self.start);
89        let end_pos = byte_offset_to_position(source, self.end);
90        (start_pos, end_pos)
91    }
92}
93
94impl fmt::Display for Span {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        write!(f, "{}..{}", self.start, self.end)
97    }
98}
99
100/// Convert a byte offset to a line/column position.
101fn byte_offset_to_position(source: &str, offset: usize) -> Position {
102    let mut line = 1;
103    let mut column = 1;
104
105    for (i, ch) in source.char_indices() {
106        if i >= offset {
107            break;
108        }
109        if ch == '\n' {
110            line += 1;
111            column = 1;
112        } else {
113            column += 1;
114        }
115    }
116
117    Position { line, column }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_position_display() {
126        let pos = Position::new(10, 25);
127        assert_eq!(pos.to_string(), "10:25");
128    }
129
130    #[test]
131    fn test_span_merge() {
132        let span1 = Span::new(5, 10);
133        let span2 = Span::new(8, 15);
134        let merged = span1.merge(span2);
135        assert_eq!(merged, Span::new(5, 15));
136    }
137
138    #[test]
139    fn test_span_len() {
140        let span = Span::new(10, 20);
141        assert_eq!(span.len(), 10);
142    }
143
144    #[test]
145    fn test_span_slice() {
146        let source = "hello world";
147        let span = Span::new(0, 5);
148        assert_eq!(span.slice(source), "hello");
149    }
150
151    #[test]
152    fn test_byte_offset_to_position() {
153        let source = "hello\nworld\nfoo";
154        assert_eq!(byte_offset_to_position(source, 0), Position::new(1, 1));
155        assert_eq!(byte_offset_to_position(source, 5), Position::new(1, 6));
156        assert_eq!(byte_offset_to_position(source, 6), Position::new(2, 1));
157        assert_eq!(byte_offset_to_position(source, 12), Position::new(3, 1));
158    }
159
160    #[test]
161    fn test_span_to_positions() {
162        let source = "hello\nworld";
163        let span = Span::new(0, 5);
164        let (start, end) = span.to_positions(source);
165        assert_eq!(start, Position::new(1, 1));
166        assert_eq!(end, Position::new(1, 6));
167    }
168}