Skip to main content

perl_position_tracking/
position.rs

1//! Enhanced position tracking for incremental parsing
2//!
3//! This module provides position and range types that track byte offsets,
4//! lines, and columns for efficient incremental parsing and error reporting.
5//!
6//! # Wire Types vs Engine Types
7//!
8//! This module defines **engine types** used internally for parsing and AST tracking.
9//! For LSP wire protocol serialization, use `crate::WirePosition` and `crate::WireRange`.
10//!
11//! Engine Position uses 1-based line/column for human-readable display.
12//! Wire Position uses 0-based line/character (UTF-16) per LSP protocol.
13
14use serde::{Deserialize, Serialize};
15use std::fmt;
16
17/// A position in a source file with byte offset, line, and column
18///
19/// This is an **engine type** for internal parsing use. It tracks byte offsets
20/// and 1-based line/column for human-friendly display.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
22pub struct Position {
23    /// Byte offset in the source (0-based)
24    pub byte: usize,
25    /// Line number (1-based for user display)
26    pub line: u32,
27    /// Column number (1-based for user display)
28    pub column: u32,
29}
30
31impl Position {
32    /// Create a new position
33    pub fn new(byte: usize, line: u32, column: u32) -> Self {
34        Position { byte, line, column }
35    }
36
37    /// Create a position at the start of a file
38    pub fn start() -> Self {
39        Position { byte: 0, line: 1, column: 1 }
40    }
41
42    /// Advance the position by the given text
43    pub fn advance(&mut self, text: &str) {
44        for ch in text.chars() {
45            if ch == '\n' {
46                self.line += 1;
47                self.column = 1;
48            } else {
49                self.column += 1;
50            }
51            self.byte += ch.len_utf8();
52        }
53    }
54
55    /// Advance by a single character
56    pub fn advance_char(&mut self, ch: char) {
57        if ch == '\n' {
58            self.line += 1;
59            self.column = 1;
60        } else {
61            self.column += 1;
62        }
63        self.byte += ch.len_utf8();
64    }
65}
66
67impl fmt::Display for Position {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        write!(f, "{}:{}", self.line, self.column)
70    }
71}
72
73/// A range in a source file defined by start and end positions
74///
75/// This is an **engine type**.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77pub struct Range {
78    /// Start position (inclusive)
79    pub start: Position,
80    /// End position (exclusive)
81    pub end: Position,
82}
83
84impl Range {
85    /// Create a new range
86    pub fn new(start: Position, end: Position) -> Self {
87        Range { start, end }
88    }
89
90    /// Create an empty range at a position
91    pub fn empty(pos: Position) -> Self {
92        Range { start: pos, end: pos }
93    }
94
95    /// Check if the range contains a byte offset
96    pub fn contains_byte(&self, byte: usize) -> bool {
97        self.start.byte <= byte && byte < self.end.byte
98    }
99
100    /// Check if the range contains a position
101    pub fn contains(&self, pos: Position) -> bool {
102        self.start.byte <= pos.byte && pos.byte < self.end.byte
103    }
104
105    /// Check if this range overlaps with another
106    pub fn overlaps(&self, other: &Range) -> bool {
107        self.start.byte < other.end.byte && other.start.byte < self.end.byte
108    }
109
110    /// Get the length in bytes
111    pub fn len(&self) -> usize {
112        self.end.byte.saturating_sub(self.start.byte)
113    }
114
115    /// Check if the range is empty
116    pub fn is_empty(&self) -> bool {
117        self.start.byte >= self.end.byte
118    }
119
120    /// Extend this range to include another range
121    pub fn extend(&mut self, other: &Range) {
122        if other.start.byte < self.start.byte {
123            self.start = other.start;
124        }
125        if other.end.byte > self.end.byte {
126            self.end = other.end;
127        }
128    }
129
130    /// Create a range that spans from this range to another
131    pub fn span_to(&self, other: &Range) -> Range {
132        Range {
133            start: if self.start.byte < other.start.byte { self.start } else { other.start },
134            end: if self.end.byte > other.end.byte { self.end } else { other.end },
135        }
136    }
137}
138
139impl fmt::Display for Range {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(f, "{}-{}", self.start, self.end)
142    }
143}
144
145/// Convert old SourceLocation to Range (for migration)
146impl From<crate::SourceLocation> for Range {
147    fn from(loc: crate::SourceLocation) -> Self {
148        // For migration, we'll need to calculate line/column later
149        Range {
150            start: Position { byte: loc.start, line: 0, column: 0 },
151            end: Position { byte: loc.end, line: 0, column: 0 },
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_position_advance() {
162        let mut pos = Position::start();
163        assert_eq!(pos, Position { byte: 0, line: 1, column: 1 });
164
165        pos.advance("hello");
166        assert_eq!(pos, Position { byte: 5, line: 1, column: 6 });
167
168        pos.advance("\n");
169        assert_eq!(pos, Position { byte: 6, line: 2, column: 1 });
170
171        pos.advance("世界"); // UTF-8 multibyte
172        assert_eq!(pos, Position { byte: 12, line: 2, column: 3 });
173    }
174
175    #[test]
176    fn test_range_operations() {
177        let start = Position::new(10, 2, 5);
178        let end = Position::new(20, 3, 10);
179        let range = Range::new(start, end);
180
181        assert!(range.contains_byte(15));
182        assert!(!range.contains_byte(25));
183        assert_eq!(range.len(), 10);
184
185        let other = Range::new(Position::new(15, 2, 10), Position::new(25, 4, 5));
186
187        assert!(range.overlaps(&other));
188
189        let span = range.span_to(&other);
190        assert_eq!(span.start.byte, 10);
191        assert_eq!(span.end.byte, 25);
192    }
193}