Skip to main content

quarto_source_map/
utils.rs

1//! Utility functions for working with source positions
2
3use crate::types::{Location, Range};
4
5/// Convert a byte offset to a Location with line and column info
6///
7/// Returns None if the offset is out of bounds.
8pub fn offset_to_location(source: &str, offset: usize) -> Option<Location> {
9    if offset > source.len() {
10        return None;
11    }
12
13    let mut row = 0;
14    let mut column = 0;
15    let mut current_offset = 0;
16
17    for ch in source.chars() {
18        if current_offset >= offset {
19            break;
20        }
21
22        if ch == '\n' {
23            row += 1;
24            column = 0;
25        } else {
26            column += 1;
27        }
28
29        current_offset += ch.len_utf8();
30    }
31
32    Some(Location {
33        offset,
34        row,
35        column,
36    })
37}
38
39/// Convert line and column numbers to a byte offset
40///
41/// Line and column are 0-indexed. Returns None if out of bounds.
42pub fn line_col_to_offset(source: &str, line: usize, col: usize) -> Option<usize> {
43    let mut current_line = 0;
44    let mut current_col = 0;
45    let mut offset = 0;
46
47    for ch in source.chars() {
48        if current_line == line && current_col == col {
49            return Some(offset);
50        }
51
52        if ch == '\n' {
53            current_line += 1;
54            current_col = 0;
55        } else {
56            current_col += 1;
57        }
58
59        offset += ch.len_utf8();
60    }
61
62    // Check if we're at the end position
63    if current_line == line && current_col == col {
64        return Some(offset);
65    }
66
67    None
68}
69
70/// Create a Range from start and end byte offsets
71///
72/// This is a helper that creates a Range with Location structs
73/// that only have offsets filled in (row and column are 0).
74/// Use `offset_to_location` to get full Location info.
75pub fn range_from_offsets(start: usize, end: usize) -> Range {
76    Range {
77        start: Location {
78            offset: start,
79            row: 0,
80            column: 0,
81        },
82        end: Location {
83            offset: end,
84            row: 0,
85            column: 0,
86        },
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_offset_to_location_simple() {
96        let source = "hello\nworld";
97
98        // Beginning
99        let loc = offset_to_location(source, 0).unwrap();
100        assert_eq!(loc.offset, 0);
101        assert_eq!(loc.row, 0);
102        assert_eq!(loc.column, 0);
103
104        // Middle of first line
105        let loc = offset_to_location(source, 3).unwrap();
106        assert_eq!(loc.offset, 3);
107        assert_eq!(loc.row, 0);
108        assert_eq!(loc.column, 3);
109
110        // After newline (beginning of second line)
111        let loc = offset_to_location(source, 6).unwrap();
112        assert_eq!(loc.offset, 6);
113        assert_eq!(loc.row, 1);
114        assert_eq!(loc.column, 0);
115
116        // Middle of second line
117        let loc = offset_to_location(source, 9).unwrap();
118        assert_eq!(loc.offset, 9);
119        assert_eq!(loc.row, 1);
120        assert_eq!(loc.column, 3);
121    }
122
123    #[test]
124    fn test_offset_to_location_out_of_bounds() {
125        let source = "hello";
126        assert!(offset_to_location(source, 100).is_none());
127    }
128
129    #[test]
130    fn test_offset_to_location_end() {
131        let source = "hello";
132        let loc = offset_to_location(source, 5).unwrap();
133        assert_eq!(loc.offset, 5);
134        assert_eq!(loc.row, 0);
135        assert_eq!(loc.column, 5);
136    }
137
138    #[test]
139    fn test_line_col_to_offset_simple() {
140        let source = "hello\nworld";
141
142        // Beginning
143        let offset = line_col_to_offset(source, 0, 0).unwrap();
144        assert_eq!(offset, 0);
145
146        // Middle of first line
147        let offset = line_col_to_offset(source, 0, 3).unwrap();
148        assert_eq!(offset, 3);
149
150        // Beginning of second line
151        let offset = line_col_to_offset(source, 1, 0).unwrap();
152        assert_eq!(offset, 6);
153
154        // Middle of second line
155        let offset = line_col_to_offset(source, 1, 3).unwrap();
156        assert_eq!(offset, 9);
157    }
158
159    #[test]
160    fn test_line_col_to_offset_out_of_bounds() {
161        let source = "hello\nworld";
162        assert!(line_col_to_offset(source, 10, 0).is_none());
163        assert!(line_col_to_offset(source, 0, 100).is_none());
164    }
165
166    #[test]
167    fn test_line_col_to_offset_end() {
168        let source = "hello";
169        let offset = line_col_to_offset(source, 0, 5).unwrap();
170        assert_eq!(offset, 5);
171    }
172
173    #[test]
174    fn test_roundtrip() {
175        let source = "hello\nworld\ntest";
176
177        // Test various positions
178        for test_offset in [0, 3, 6, 10, 16] {
179            let loc = offset_to_location(source, test_offset).unwrap();
180            let back_to_offset = line_col_to_offset(source, loc.row, loc.column).unwrap();
181            assert_eq!(test_offset, back_to_offset);
182        }
183    }
184
185    #[test]
186    fn test_range_from_offsets() {
187        let range = range_from_offsets(10, 20);
188        assert_eq!(range.start.offset, 10);
189        assert_eq!(range.end.offset, 20);
190        assert_eq!(range.start.row, 0);
191        assert_eq!(range.start.column, 0);
192    }
193
194    #[test]
195    fn test_offset_to_location_multiline() {
196        let source = "line1\nline2\nline3";
197
198        // Test each line start
199        let loc = offset_to_location(source, 0).unwrap();
200        assert_eq!(loc.row, 0);
201        assert_eq!(loc.column, 0);
202
203        let loc = offset_to_location(source, 6).unwrap();
204        assert_eq!(loc.row, 1);
205        assert_eq!(loc.column, 0);
206
207        let loc = offset_to_location(source, 12).unwrap();
208        assert_eq!(loc.row, 2);
209        assert_eq!(loc.column, 0);
210    }
211}