1#![forbid(unsafe_op_in_unsafe_fn)]
6#![deny(missing_docs)]
7#![cfg_attr(feature = "strict_api", deny(unreachable_pub))]
8#![cfg_attr(not(feature = "strict_api"), warn(unreachable_pub))]
9#![cfg_attr(feature = "strict_docs", deny(missing_docs))]
10#![cfg_attr(not(feature = "strict_docs"), allow(missing_docs))]
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct LineCol {
25 pub line: usize,
27 pub line_start: usize,
29}
30
31impl LineCol {
32 #[must_use]
44 pub const fn new() -> Self {
45 Self {
46 line: 0,
47 line_start: 0,
48 }
49 }
50
51 #[must_use]
66 pub fn at_position(input: &[u8], position: usize) -> Self {
67 let mut tracker = Self::new();
68 let end = position.min(input.len());
69
70 for i in 0..end {
71 if input[i] == b'\n' {
72 tracker.advance_line(i + 1);
73 } else if input[i] == b'\r' {
74 if i + 1 < input.len() && input[i + 1] == b'\n' {
76 continue;
77 }
78 tracker.advance_line(i + 1);
79 }
80 }
81
82 tracker
83 }
84
85 pub fn advance_line(&mut self, new_line_start: usize) {
98 self.line += 1;
99 self.line_start = new_line_start;
100 }
101
102 pub fn process_byte(&mut self, byte: u8, next_byte: Option<u8>, current_offset: usize) -> bool {
120 match byte {
121 b'\n' => {
122 self.advance_line(current_offset + 1);
123 true
124 }
125 b'\r' => {
126 if next_byte == Some(b'\n') {
127 false
128 } else {
129 self.advance_line(current_offset + 1);
130 true
131 }
132 }
133 _ => false,
134 }
135 }
136
137 #[must_use]
149 pub fn column(&self, position: usize) -> usize {
150 position.saturating_sub(self.line_start)
151 }
152}
153
154impl std::fmt::Display for LineCol {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 write!(f, "line {}, col {}", self.line, self.line_start)
157 }
158}
159
160impl Default for LineCol {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn basic_newline_tracking() {
172 let input = b"hello\nworld\n";
173 let tracker = LineCol::at_position(input, 6);
174 assert_eq!(tracker.line, 1);
175 assert_eq!(tracker.line_start, 6);
176 assert_eq!(tracker.column(8), 2);
177 }
178
179 #[test]
180 fn crlf_handling() {
181 let input = b"hello\r\nworld\r\n";
182 let tracker = LineCol::at_position(input, 7);
183 assert_eq!(tracker.line, 1);
184 assert_eq!(tracker.line_start, 7);
185 assert_eq!(tracker.column(9), 2);
186 }
187
188 #[test]
189 fn cr_only_handling() {
190 let input = b"hello\rworld\r";
191 let tracker = LineCol::at_position(input, 6);
192 assert_eq!(tracker.line, 1);
193 assert_eq!(tracker.line_start, 6);
194 }
195
196 #[test]
197 fn process_byte_tracks_line_boundaries() {
198 let mut tracker = LineCol::new();
199
200 assert!(!tracker.process_byte(b'a', None, 0));
201 assert_eq!(tracker.line, 0);
202
203 assert!(tracker.process_byte(b'\n', None, 5));
204 assert_eq!(tracker.line, 1);
205 assert_eq!(tracker.line_start, 6);
206
207 assert!(tracker.process_byte(b'\r', Some(b'x'), 10));
208 assert_eq!(tracker.line, 2);
209 assert_eq!(tracker.line_start, 11);
210
211 assert!(!tracker.process_byte(b'\r', Some(b'\n'), 15));
212 assert_eq!(tracker.line, 2);
213 }
214
215 #[test]
218 fn column_at_line_start_is_zero() {
219 let tracker = LineCol::at_position(b"ab\ncd", 3);
220 assert_eq!(tracker.column(3), 0);
221 }
222
223 #[test]
224 fn column_saturates_when_position_below_line_start() {
225 let tracker = LineCol {
226 line: 1,
227 line_start: 10,
228 };
229 assert_eq!(tracker.column(5), 0);
230 }
231
232 #[test]
233 fn advance_line_increments_line_by_exactly_one() {
234 let mut tracker = LineCol::new();
235 tracker.advance_line(5);
236 assert_eq!(tracker.line, 1);
237 assert_eq!(tracker.line_start, 5);
238 tracker.advance_line(10);
239 assert_eq!(tracker.line, 2);
240 assert_eq!(tracker.line_start, 10);
241 }
242
243 #[test]
244 fn at_position_zero_returns_initial_state() {
245 let tracker = LineCol::at_position(b"hello\nworld", 0);
246 assert_eq!(tracker.line, 0);
247 assert_eq!(tracker.line_start, 0);
248 }
249
250 #[test]
251 fn at_position_just_past_newline() {
252 let input = b"a\nb";
253 let before = LineCol::at_position(input, 1);
254 let after = LineCol::at_position(input, 2);
255 assert_eq!(before.line, 0);
256 assert_eq!(after.line, 1);
257 assert_eq!(after.line_start, 2);
258 }
259
260 #[test]
261 fn multiple_consecutive_newlines() {
262 let input = b"\n\n\n";
263 let tracker = LineCol::at_position(input, 3);
264 assert_eq!(tracker.line, 3);
265 assert_eq!(tracker.line_start, 3);
266 }
267
268 #[test]
269 fn new_fields_are_zero() {
270 let tracker = LineCol::new();
271 assert_eq!(tracker.line, 0);
272 assert_eq!(tracker.line_start, 0);
273 }
274}