1#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct Span {
33 pub file: String,
35
36 pub start_line: u32,
38
39 pub start_column: u32,
41
42 pub end_line: u32,
44
45 pub end_column: u32,
51}
52
53impl Span {
54 pub fn new(
74 file: String,
75 start_line: u32,
76 start_column: u32,
77 end_line: u32,
78 end_column: u32,
79 ) -> Self {
80 assert!(start_line > 0, "Line numbers are 1-indexed");
81 assert!(start_column > 0, "Column numbers are 1-indexed");
82 assert!(end_line > 0, "Line numbers are 1-indexed");
83 assert!(end_column > 0, "Column numbers are 1-indexed");
84 assert!(end_line >= start_line, "End line must be >= start line");
85 if end_line == start_line {
86 assert!(end_column > start_column, "End column must be > start column on same line");
87 }
88
89 Self { file, start_line, start_column, end_line, end_column }
90 }
91
92 pub fn repl(start_line: u32, start_column: u32, end_line: u32, end_column: u32) -> Self {
103 Self::new("<repl>".to_string(), start_line, start_column, end_line, end_column)
104 }
105
106 pub fn is_single_line(&self) -> bool {
120 self.start_line == self.end_line
121 }
122
123 pub fn is_multi_line(&self) -> bool {
125 !self.is_single_line()
126 }
127
128 pub fn num_lines(&self) -> u32 {
139 self.end_line - self.start_line + 1
140 }
141
142 pub fn length_on_start_line(&self) -> u32 {
156 if self.is_single_line() {
157 self.end_column - self.start_column
158 } else {
159 0
162 }
163 }
164
165 pub fn contains(&self, other: &Span) -> bool {
179 if self.file != other.file {
180 return false;
181 }
182
183 let start_ok = other.start_line > self.start_line
185 || (other.start_line == self.start_line && other.start_column >= self.start_column);
186
187 let end_ok = other.end_line < self.end_line
189 || (other.end_line == self.end_line && other.end_column <= self.end_column);
190
191 start_ok && end_ok
192 }
193
194 pub fn merge(&self, other: &Span) -> Span {
215 assert_eq!(self.file, other.file, "Cannot merge spans from different files");
216
217 let (start_line, start_column) = if self.start_line < other.start_line
218 || (self.start_line == other.start_line && self.start_column < other.start_column)
219 {
220 (self.start_line, self.start_column)
221 } else {
222 (other.start_line, other.start_column)
223 };
224
225 let (end_line, end_column) = if self.end_line > other.end_line
226 || (self.end_line == other.end_line && self.end_column > other.end_column)
227 {
228 (self.end_line, self.end_column)
229 } else {
230 (other.end_line, other.end_column)
231 };
232
233 Span::new(self.file.clone(), start_line, start_column, end_line, end_column)
234 }
235}
236
237impl std::fmt::Display for Span {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 if self.is_single_line() {
240 write!(f, "{}:{}:{}-{}", self.file, self.start_line, self.start_column, self.end_column)
241 } else {
242 write!(
243 f,
244 "{}:{}:{}-{}:{}",
245 self.file, self.start_line, self.start_column, self.end_line, self.end_column
246 )
247 }
248 }
249}
250
251impl From<&Span> for crate::SourcePos {
257 fn from(span: &Span) -> Self {
258 assert!(span.is_single_line(), "Can only convert single-line Span to SourcePos");
259 crate::SourcePos::new(
260 span.file.clone(),
261 span.start_line,
262 span.start_column,
263 span.length_on_start_line(),
264 )
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_span_basic() {
274 let span = Span::new("test.oxur".to_string(), 1, 5, 1, 15);
275 assert_eq!(span.file, "test.oxur");
276 assert_eq!(span.start_line, 1);
277 assert_eq!(span.start_column, 5);
278 assert_eq!(span.end_line, 1);
279 assert_eq!(span.end_column, 15);
280 }
281
282 #[test]
283 fn test_span_display_single_line() {
284 let span = Span::new("test.oxur".to_string(), 10, 5, 10, 15);
285 assert_eq!(format!("{}", span), "test.oxur:10:5-15");
286 }
287
288 #[test]
289 fn test_span_display_multi_line() {
290 let span = Span::new("test.oxur".to_string(), 1, 5, 3, 10);
291 assert_eq!(format!("{}", span), "test.oxur:1:5-3:10");
292 }
293
294 #[test]
295 fn test_span_repl() {
296 let span = Span::repl(1, 1, 1, 20);
297 assert_eq!(span.file, "<repl>");
298 assert_eq!(span.start_line, 1);
299 }
300
301 #[test]
302 fn test_span_is_single_line() {
303 let single = Span::new("test.oxur".to_string(), 1, 5, 1, 15);
304 assert!(single.is_single_line());
305 assert!(!single.is_multi_line());
306
307 let multi = Span::new("test.oxur".to_string(), 1, 5, 3, 10);
308 assert!(!multi.is_single_line());
309 assert!(multi.is_multi_line());
310 }
311
312 #[test]
313 fn test_span_num_lines() {
314 let span1 = Span::new("test.oxur".to_string(), 1, 5, 1, 15);
315 assert_eq!(span1.num_lines(), 1);
316
317 let span2 = Span::new("test.oxur".to_string(), 1, 5, 3, 10);
318 assert_eq!(span2.num_lines(), 3);
319
320 let span3 = Span::new("test.oxur".to_string(), 5, 1, 10, 1);
321 assert_eq!(span3.num_lines(), 6);
322 }
323
324 #[test]
325 fn test_span_length_on_start_line() {
326 let single = Span::new("test.oxur".to_string(), 1, 5, 1, 15);
327 assert_eq!(single.length_on_start_line(), 10);
328
329 let multi = Span::new("test.oxur".to_string(), 1, 5, 3, 10);
330 assert_eq!(multi.length_on_start_line(), 0);
332 }
333
334 #[test]
335 fn test_span_contains_same_line() {
336 let outer = Span::new("test.oxur".to_string(), 1, 5, 1, 20);
337 let inner = Span::new("test.oxur".to_string(), 1, 10, 1, 15);
338 let before = Span::new("test.oxur".to_string(), 1, 1, 1, 4);
339 let after = Span::new("test.oxur".to_string(), 1, 21, 1, 25);
340
341 assert!(outer.contains(&inner));
342 assert!(!outer.contains(&before));
343 assert!(!outer.contains(&after));
344 assert!(!inner.contains(&outer));
345 }
346
347 #[test]
348 fn test_span_contains_multi_line() {
349 let outer = Span::new("test.oxur".to_string(), 1, 5, 5, 20);
350 let inner = Span::new("test.oxur".to_string(), 2, 1, 3, 10);
351 let overlapping = Span::new("test.oxur".to_string(), 1, 1, 2, 10);
352
353 assert!(outer.contains(&inner));
354 assert!(!outer.contains(&overlapping)); assert!(!inner.contains(&outer));
356 }
357
358 #[test]
359 fn test_span_contains_different_files() {
360 let span1 = Span::new("file1.oxur".to_string(), 1, 5, 1, 20);
361 let span2 = Span::new("file2.oxur".to_string(), 1, 10, 1, 15);
362
363 assert!(!span1.contains(&span2));
364 }
365
366 #[test]
367 fn test_span_merge_same_line() {
368 let span1 = Span::new("test.oxur".to_string(), 1, 5, 1, 10);
369 let span2 = Span::new("test.oxur".to_string(), 1, 15, 1, 20);
370 let merged = span1.merge(&span2);
371
372 assert_eq!(merged.start_line, 1);
373 assert_eq!(merged.start_column, 5);
374 assert_eq!(merged.end_line, 1);
375 assert_eq!(merged.end_column, 20);
376 }
377
378 #[test]
379 fn test_span_merge_multi_line() {
380 let span1 = Span::new("test.oxur".to_string(), 1, 5, 2, 10);
381 let span2 = Span::new("test.oxur".to_string(), 3, 1, 4, 5);
382 let merged = span1.merge(&span2);
383
384 assert_eq!(merged.start_line, 1);
385 assert_eq!(merged.start_column, 5);
386 assert_eq!(merged.end_line, 4);
387 assert_eq!(merged.end_column, 5);
388 }
389
390 #[test]
391 fn test_span_merge_overlapping() {
392 let span1 = Span::new("test.oxur".to_string(), 1, 5, 2, 10);
393 let span2 = Span::new("test.oxur".to_string(), 2, 1, 3, 5);
394 let merged = span1.merge(&span2);
395
396 assert_eq!(merged.start_line, 1);
397 assert_eq!(merged.start_column, 5);
398 assert_eq!(merged.end_line, 3);
399 assert_eq!(merged.end_column, 5);
400 }
401
402 #[test]
403 fn test_span_merge_reversed() {
404 let span1 = Span::new("test.oxur".to_string(), 3, 1, 4, 5);
405 let span2 = Span::new("test.oxur".to_string(), 1, 5, 2, 10);
406 let merged = span1.merge(&span2);
407
408 assert_eq!(merged.start_line, 1);
410 assert_eq!(merged.start_column, 5);
411 assert_eq!(merged.end_line, 4);
412 assert_eq!(merged.end_column, 5);
413 }
414
415 #[test]
416 #[should_panic(expected = "Cannot merge spans from different files")]
417 fn test_span_merge_different_files() {
418 let span1 = Span::new("file1.oxur".to_string(), 1, 5, 1, 10);
419 let span2 = Span::new("file2.oxur".to_string(), 1, 15, 1, 20);
420 span1.merge(&span2);
421 }
422
423 #[test]
424 #[should_panic(expected = "Line numbers are 1-indexed")]
425 fn test_span_zero_start_line() {
426 Span::new("test.oxur".to_string(), 0, 1, 1, 10);
427 }
428
429 #[test]
430 #[should_panic(expected = "Column numbers are 1-indexed")]
431 fn test_span_zero_start_column() {
432 Span::new("test.oxur".to_string(), 1, 0, 1, 10);
433 }
434
435 #[test]
436 #[should_panic(expected = "Line numbers are 1-indexed")]
437 fn test_span_zero_end_line() {
438 Span::new("test.oxur".to_string(), 1, 1, 0, 10);
439 }
440
441 #[test]
442 #[should_panic(expected = "Column numbers are 1-indexed")]
443 fn test_span_zero_end_column() {
444 Span::new("test.oxur".to_string(), 1, 1, 1, 0);
445 }
446
447 #[test]
448 #[should_panic(expected = "End line must be >= start line")]
449 fn test_span_end_before_start_line() {
450 Span::new("test.oxur".to_string(), 5, 1, 3, 10);
451 }
452
453 #[test]
454 #[should_panic(expected = "End column must be > start column on same line")]
455 fn test_span_end_before_start_column() {
456 Span::new("test.oxur".to_string(), 1, 10, 1, 5);
457 }
458
459 #[test]
460 #[should_panic(expected = "End column must be > start column on same line")]
461 fn test_span_equal_positions() {
462 Span::new("test.oxur".to_string(), 1, 10, 1, 10);
463 }
464
465 #[test]
466 fn test_span_to_source_pos() {
467 let span = Span::new("test.oxur".to_string(), 1, 5, 1, 15);
468 let pos: crate::SourcePos = (&span).into();
469
470 assert_eq!(pos.file, "test.oxur");
471 assert_eq!(pos.line, 1);
472 assert_eq!(pos.column, 5);
473 assert_eq!(pos.length, 10);
474 }
475
476 #[test]
477 #[should_panic(expected = "Can only convert single-line Span to SourcePos")]
478 fn test_span_to_source_pos_multi_line() {
479 let span = Span::new("test.oxur".to_string(), 1, 5, 3, 10);
480 let _pos: crate::SourcePos = (&span).into();
481 }
482}