Skip to main content

speed_reader_core/
position.rs

1/// Маппинг токена в исходную позицию текста.
2/// Requirement: 2.2, Design: PositionTracker section
3
4#[derive(Debug, Clone, PartialEq)]
5pub struct SourcePosition {
6    pub byte_offset: usize,
7    pub line: usize,
8    pub column: usize,
9    pub context: String,
10}
11
12pub struct PositionTracker {
13    line_offsets: Vec<usize>,
14    text: String,
15}
16
17impl PositionTracker {
18    pub fn new(text: &str) -> Self {
19        let mut line_offsets = vec![0usize];
20        for (i, c) in text.char_indices() {
21            if c == '\n' {
22                line_offsets.push(i + 1);
23            }
24        }
25        Self {
26            line_offsets,
27            text: text.to_string(),
28        }
29    }
30
31    /// Возвращает позицию в исходном тексте по байтовому смещению.
32    pub fn position_for_offset(&self, byte_offset: usize) -> SourcePosition {
33        let byte_offset = byte_offset.min(self.text.len());
34        // binary search for the line
35        let line = match self.line_offsets.binary_search(&byte_offset) {
36            Ok(line_idx) => line_idx + 1,
37            Err(line_idx) => line_idx, // line_idx is where it would be inserted = current line
38        };
39        let line = line.max(1);
40        let line_start = self.line_offsets[line - 1];
41        let column = byte_offset - line_start;
42
43        // Grab context line
44        let line_end = if line < self.line_offsets.len() {
45            self.line_offsets[line]
46        } else {
47            self.text.len()
48        };
49        let context = self.text[line_start..line_end]
50            .trim_end_matches('\n')
51            .trim_end_matches('\r')
52            .to_string();
53
54        SourcePosition {
55            byte_offset,
56            line,
57            column,
58            context,
59        }
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn test_single_line() {
69        let tracker = PositionTracker::new("Hello world");
70        let pos = tracker.position_for_offset(6);
71        assert_eq!(pos.line, 1);
72        assert_eq!(pos.column, 6);
73        assert_eq!(pos.context, "Hello world");
74    }
75
76    #[test]
77    fn test_multi_line() {
78        let tracker = PositionTracker::new("Line one\nLine two\nLine three");
79        let pos = tracker.position_for_offset(14); // "Line two" starts at 9, we want 't' in "two"
80        assert_eq!(pos.line, 2);
81        assert_eq!(pos.column, 5);
82        assert_eq!(pos.context, "Line two");
83    }
84
85    #[test]
86    fn test_first_line() {
87        let tracker = PositionTracker::new("Hello\nWorld");
88        let pos = tracker.position_for_offset(0);
89        assert_eq!(pos.line, 1);
90        assert_eq!(pos.column, 0);
91    }
92
93    #[test]
94    fn test_beyond_text() {
95        let tracker = PositionTracker::new("Hello");
96        let pos = tracker.position_for_offset(100);
97        assert_eq!(pos.byte_offset, 5); // clamped
98    }
99}