oxur_smap/
source_pos.rs

1/// Source position in original Oxur code
2#[derive(Debug, Clone, PartialEq, Eq)]
3pub struct SourcePos {
4    /// Source file path (or "\<repl\>" for REPL input)
5    pub file: String,
6
7    /// 1-indexed line number
8    pub line: u32,
9
10    /// 1-indexed column number
11    pub column: u32,
12
13    /// Length of the span (for error highlighting)
14    pub length: u32,
15}
16
17impl SourcePos {
18    /// Create a new source position
19    pub fn new(file: String, line: u32, column: u32, length: u32) -> Self {
20        assert!(line > 0, "Line numbers are 1-indexed");
21        assert!(column > 0, "Column numbers are 1-indexed");
22        Self { file, line, column, length }
23    }
24
25    /// Create a position for REPL input
26    pub fn repl(line: u32, column: u32, length: u32) -> Self {
27        Self::new("<repl>".to_string(), line, column, length)
28    }
29
30    /// Get end column (column + length)
31    pub fn end_column(&self) -> u32 {
32        self.column + self.length
33    }
34
35    /// Check if this position contains another position
36    pub fn contains(&self, other: &SourcePos) -> bool {
37        self.file == other.file
38            && self.line == other.line
39            && other.column >= self.column
40            && other.column < self.end_column()
41    }
42}
43
44impl std::fmt::Display for SourcePos {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(f, "{}:{}:{}", self.file, self.line, self.column)
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_source_pos_basic() {
56        let pos = SourcePos::new("test.oxur".to_string(), 1, 5, 10);
57        assert_eq!(pos.line, 1);
58        assert_eq!(pos.column, 5);
59        assert_eq!(pos.length, 10);
60        assert_eq!(pos.end_column(), 15);
61    }
62
63    #[test]
64    fn test_source_pos_display() {
65        let pos = SourcePos::new("test.oxur".to_string(), 10, 20, 5);
66        assert_eq!(format!("{}", pos), "test.oxur:10:20");
67    }
68
69    #[test]
70    fn test_source_pos_repl() {
71        let pos = SourcePos::repl(1, 1, 20);
72        assert_eq!(pos.file, "<repl>");
73        assert_eq!(pos.line, 1);
74        assert_eq!(pos.column, 1);
75        assert_eq!(pos.length, 20);
76    }
77
78    #[test]
79    fn test_source_pos_contains() {
80        let span = SourcePos::new("test.oxur".to_string(), 1, 5, 10);
81        let inner = SourcePos::new("test.oxur".to_string(), 1, 7, 3);
82        let outer = SourcePos::new("test.oxur".to_string(), 1, 3, 2);
83
84        assert!(span.contains(&inner));
85        assert!(!span.contains(&outer));
86    }
87
88    #[test]
89    fn test_source_pos_contains_different_files() {
90        let span1 = SourcePos::new("file1.oxur".to_string(), 1, 5, 10);
91        let span2 = SourcePos::new("file2.oxur".to_string(), 1, 7, 3);
92
93        assert!(!span1.contains(&span2));
94    }
95
96    #[test]
97    fn test_source_pos_contains_different_lines() {
98        let span1 = SourcePos::new("test.oxur".to_string(), 1, 5, 10);
99        let span2 = SourcePos::new("test.oxur".to_string(), 2, 7, 3);
100
101        assert!(!span1.contains(&span2));
102    }
103
104    #[test]
105    #[should_panic(expected = "Line numbers are 1-indexed")]
106    fn test_source_pos_zero_line() {
107        SourcePos::new("test.oxur".to_string(), 0, 1, 1);
108    }
109
110    #[test]
111    #[should_panic(expected = "Column numbers are 1-indexed")]
112    fn test_source_pos_zero_column() {
113        SourcePos::new("test.oxur".to_string(), 1, 0, 1);
114    }
115}