Skip to main content

oxilean_parse/span_util/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use crate::tokens::Span;
6
7use super::types::{PrioritizedSpan, SourcePos};
8
9/// Create a dummy span for testing (line 1, column 1, zero-length).
10#[allow(missing_docs)]
11pub fn dummy_span() -> Span {
12    Span::new(0, 0, 1, 1)
13}
14/// Check if a span contains a byte position.
15#[allow(missing_docs)]
16pub fn span_contains(span: &Span, pos: usize) -> bool {
17    pos >= span.start && pos < span.end
18}
19/// Merge multiple spans into one encompassing span.
20///
21/// Returns `None` if the slice is empty.
22#[allow(missing_docs)]
23pub fn merge_spans(spans: &[Span]) -> Option<Span> {
24    if spans.is_empty() {
25        return None;
26    }
27    let mut result = spans[0].clone();
28    for span in &spans[1..] {
29        result = result.merge(span);
30    }
31    Some(result)
32}
33/// Get the byte-length of a span.
34#[allow(missing_docs)]
35pub fn span_len(span: &Span) -> usize {
36    span.end.saturating_sub(span.start)
37}
38/// Extract the `SourcePos` from a `Span` (start of span).
39#[allow(missing_docs)]
40pub fn span_start_pos(span: &Span) -> SourcePos {
41    SourcePos::new(span.line, span.column)
42}
43/// Extract the start byte offset from a `Span`.
44#[allow(missing_docs)]
45pub fn span_start(span: &Span) -> usize {
46    span.start
47}
48/// Extract the end byte offset from a `Span`.
49#[allow(missing_docs)]
50pub fn span_end(span: &Span) -> usize {
51    span.end
52}
53/// Return a span that begins immediately after `span` ends (zero-length).
54#[allow(missing_docs)]
55pub fn span_after(span: &Span) -> Span {
56    Span::new(span.end, span.end, span.line, span.column + span_len(span))
57}
58/// Return a span that begins one byte before `span` starts (zero-length).
59///
60/// Saturates at zero — will not produce negative offsets.
61#[allow(missing_docs)]
62pub fn span_before(span: &Span) -> Span {
63    let start = span.start.saturating_sub(1);
64    let col = span.column.saturating_sub(1).max(1);
65    Span::new(start, start, span.line, col)
66}
67/// Extend `span` by `n` bytes to the right (clamped to `source_len`).
68#[allow(missing_docs)]
69pub fn span_extend(span: &Span, n: usize, source_len: usize) -> Span {
70    let new_end = (span.end + n).min(source_len);
71    Span::new(span.start, new_end, span.line, span.column)
72}
73/// Shrink a span from the left by `n` bytes.
74#[allow(missing_docs)]
75pub fn span_shrink_left(span: &Span, n: usize) -> Span {
76    let new_start = (span.start + n).min(span.end);
77    Span::new(new_start, span.end, span.line, span.column + n)
78}
79/// Shrink a span from the right by `n` bytes.
80#[allow(missing_docs)]
81pub fn span_shrink_right(span: &Span, n: usize) -> Span {
82    let new_end = span.end.saturating_sub(n).max(span.start);
83    Span::new(span.start, new_end, span.line, span.column)
84}
85/// Return `true` if `a` and `b` overlap (share at least one byte).
86#[allow(missing_docs)]
87pub fn spans_overlap(a: &Span, b: &Span) -> bool {
88    a.start < b.end && b.start < a.end
89}
90/// Return `true` if `outer` completely contains `inner`.
91#[allow(missing_docs)]
92pub fn span_contains_span(outer: &Span, inner: &Span) -> bool {
93    outer.start <= inner.start && inner.end <= outer.end
94}
95/// Return the intersection of two spans, or `None` if they do not overlap.
96#[allow(missing_docs)]
97pub fn span_intersection(a: &Span, b: &Span) -> Option<Span> {
98    let start = a.start.max(b.start);
99    let end = a.end.min(b.end);
100    if start < end {
101        Some(Span::new(start, end, a.line, a.column))
102    } else {
103        None
104    }
105}
106/// Extract the source text covered by `span` from a full source `&str`.
107///
108/// Returns `""` if the span is out of bounds.
109#[allow(missing_docs)]
110pub fn extract_span<'a>(source: &'a str, span: &Span) -> &'a str {
111    source.get(span.start..span.end).unwrap_or("")
112}
113/// Return the entire line that contains `span` from `source`.
114///
115/// Scans backwards from `span.start` to find the line start.
116#[allow(missing_docs)]
117pub fn extract_line<'a>(source: &'a str, span: &Span) -> &'a str {
118    let bytes = source.as_bytes();
119    let line_start = bytes[..span.start]
120        .iter()
121        .rposition(|&b| b == b'\n')
122        .map(|i| i + 1)
123        .unwrap_or(0);
124    let line_end = bytes[span.start..]
125        .iter()
126        .position(|&b| b == b'\n')
127        .map(|i| span.start + i)
128        .unwrap_or(source.len());
129    &source[line_start..line_end]
130}
131/// Count the number of lines in a source string.
132#[allow(missing_docs)]
133pub fn count_lines(source: &str) -> usize {
134    source.bytes().filter(|&b| b == b'\n').count() + 1
135}
136/// Compute a `Span` from a (line, col) pair by scanning the source string.
137///
138/// Line and column are 1-indexed.  Returns `None` if out of range.
139#[allow(missing_docs)]
140pub fn pos_to_span(source: &str, line: usize, col: usize) -> Option<Span> {
141    let mut cur_line = 1usize;
142    let mut cur_col = 1usize;
143    for (i, ch) in source.char_indices() {
144        if cur_line == line && cur_col == col {
145            let end = i + ch.len_utf8();
146            return Some(Span::new(i, end, line, col));
147        }
148        if ch == '\n' {
149            cur_line += 1;
150            cur_col = 1;
151        } else {
152            cur_col += 1;
153        }
154    }
155    None
156}
157/// Format a span as `line:col` for short diagnostic messages.
158#[allow(missing_docs)]
159pub fn span_short(span: &Span) -> String {
160    format!("{}:{}", span.line, span.column)
161}
162/// Format a span as `line:col-end` showing the end column too.
163#[allow(missing_docs)]
164pub fn span_range(span: &Span) -> String {
165    let end_col = span.column + span_len(span);
166    format!("{}:{}-{}", span.line, span.column, end_col)
167}
168/// Build a caret string (`^~~~~`) pointing at a span within its line.
169///
170/// `col` is 1-indexed; `len` is the number of `~` characters after the first `^`.
171#[allow(missing_docs)]
172pub fn span_caret(col: usize, len: usize) -> String {
173    let spaces = " ".repeat(col.saturating_sub(1));
174    let carets = if len == 0 {
175        "^".to_string()
176    } else {
177        let mut s = "^".to_string();
178        s.push_str(&"~".repeat(len.saturating_sub(1)));
179        s
180    };
181    format!("{}{}", spaces, carets)
182}
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::span_util::*;
187    #[test]
188    fn test_dummy_span() {
189        let span = dummy_span();
190        assert_eq!(span.line, 1);
191        assert_eq!(span.column, 1);
192    }
193    #[test]
194    fn test_span_contains() {
195        let span = Span::new(10, 20, 1, 1);
196        assert!(span_contains(&span, 10));
197        assert!(span_contains(&span, 15));
198        assert!(!span_contains(&span, 20));
199        assert!(!span_contains(&span, 5));
200    }
201    #[test]
202    fn test_merge_spans() {
203        let s1 = Span::new(0, 5, 1, 1);
204        let s2 = Span::new(10, 15, 1, 11);
205        let s3 = Span::new(20, 25, 2, 1);
206        let merged = merge_spans(&[s1, s2, s3]).expect("span should be present");
207        assert_eq!(merged.start, 0);
208        assert_eq!(merged.end, 25);
209    }
210    #[test]
211    fn test_span_len() {
212        let span = Span::new(10, 25, 1, 1);
213        assert_eq!(span_len(&span), 15);
214    }
215    #[test]
216    fn test_source_pos_display() {
217        let pos = SourcePos::new(3, 7);
218        assert_eq!(pos.to_string(), "3:7");
219    }
220    #[test]
221    fn test_source_pos_advance() {
222        let pos = SourcePos::start();
223        let next = pos.advance_col();
224        assert_eq!(next.col, 2);
225        let nl = pos.advance_line();
226        assert_eq!(nl.line, 2);
227        assert_eq!(nl.col, 1);
228    }
229    #[test]
230    fn test_spans_overlap() {
231        let a = Span::new(0, 10, 1, 1);
232        let b = Span::new(5, 15, 1, 6);
233        let c = Span::new(10, 20, 1, 11);
234        assert!(spans_overlap(&a, &b));
235        assert!(!spans_overlap(&a, &c));
236    }
237    #[test]
238    fn test_span_contains_span() {
239        let outer = Span::new(0, 20, 1, 1);
240        let inner = Span::new(5, 15, 1, 6);
241        let too_big = Span::new(5, 25, 1, 6);
242        assert!(span_contains_span(&outer, &inner));
243        assert!(!span_contains_span(&outer, &too_big));
244    }
245    #[test]
246    fn test_span_intersection() {
247        let a = Span::new(0, 10, 1, 1);
248        let b = Span::new(5, 15, 1, 6);
249        let isect = span_intersection(&a, &b).expect("span should be present");
250        assert_eq!(isect.start, 5);
251        assert_eq!(isect.end, 10);
252        let c = Span::new(20, 30, 2, 1);
253        assert!(span_intersection(&a, &c).is_none());
254    }
255    #[test]
256    fn test_extract_span() {
257        let source = "hello world";
258        let span = Span::new(6, 11, 1, 7);
259        assert_eq!(extract_span(source, &span), "world");
260    }
261    #[test]
262    fn test_extract_line() {
263        let source = "line one\nline two\nline three";
264        let span = Span::new(9, 13, 2, 1);
265        assert_eq!(extract_line(source, &span), "line two");
266    }
267    #[test]
268    fn test_count_lines() {
269        assert_eq!(count_lines("a\nb\nc"), 3);
270        assert_eq!(count_lines("no newlines"), 1);
271    }
272    #[test]
273    fn test_span_caret() {
274        let c = span_caret(5, 3);
275        assert!(c.starts_with("    ^"));
276    }
277    #[test]
278    fn test_span_builder() {
279        let builder = SpanBuilder::new(10, 2, 5);
280        let span = builder.finish(20);
281        assert_eq!(span.start, 10);
282        assert_eq!(span.end, 20);
283        assert_eq!(span.line, 2);
284    }
285    #[test]
286    fn test_spanned_map() {
287        let s = Spanned::new(42u32, dummy_span());
288        let s2 = s.map(|v| v * 2);
289        assert_eq!(s2.value, 84);
290    }
291    #[test]
292    fn test_labeled_span() {
293        let span = Span::new(0, 5, 1, 1);
294        let ls = LabeledSpan::new("here", span);
295        assert_eq!(ls.label, "here");
296        assert_eq!(ls.len(), 5);
297        assert!(!ls.is_empty());
298    }
299    #[test]
300    fn test_span_short_and_range() {
301        let span = Span::new(10, 15, 3, 7);
302        assert_eq!(span_short(&span), "3:7");
303        let r = span_range(&span);
304        assert!(r.contains("3:7"));
305    }
306    #[test]
307    fn test_span_extend_shrink() {
308        let span = Span::new(5, 10, 1, 6);
309        let extended = span_extend(&span, 3, 100);
310        assert_eq!(extended.end, 13);
311        let shrunk = span_shrink_right(&span, 2);
312        assert_eq!(shrunk.end, 8);
313        let shrunk_l = span_shrink_left(&span, 2);
314        assert_eq!(shrunk_l.start, 7);
315    }
316    #[test]
317    fn test_merge_empty() {
318        assert!(merge_spans(&[]).is_none());
319    }
320    #[test]
321    fn test_span_before_after() {
322        let span = Span::new(10, 15, 1, 11);
323        let after = span_after(&span);
324        assert_eq!(after.start, 15);
325        let before = span_before(&span);
326        assert_eq!(before.start, 9);
327    }
328}
329/// Return `true` if `a` starts before `b` (by byte offset).
330#[allow(missing_docs)]
331pub fn span_before_other(a: &Span, b: &Span) -> bool {
332    a.start < b.start
333}
334/// Sort a slice of spans by start offset.
335#[allow(missing_docs)]
336pub fn sort_spans(spans: &mut [Span]) {
337    spans.sort_by_key(|s| s.start);
338}
339/// Merge overlapping/adjacent spans in a sorted list.
340#[allow(missing_docs)]
341pub fn coalesce_spans(spans: &[Span]) -> Vec<Span> {
342    if spans.is_empty() {
343        return vec![];
344    }
345    let mut result: Vec<Span> = Vec::new();
346    let mut current = spans[0].clone();
347    for span in &spans[1..] {
348        if span.start <= current.end {
349            current = current.merge(span);
350        } else {
351            result.push(current.clone());
352            current = span.clone();
353        }
354    }
355    result.push(current);
356    result
357}
358/// Compute the gap (non-overlapping region) between two non-overlapping spans.
359///
360/// Returns `None` if the spans overlap.
361#[allow(missing_docs)]
362pub fn span_gap(a: &Span, b: &Span) -> Option<Span> {
363    let (first, second) = if a.end <= b.start { (a, b) } else { (b, a) };
364    if first.end < second.start {
365        Some(Span::new(
366            first.end,
367            second.start,
368            first.line,
369            first.column + span_len(first),
370        ))
371    } else {
372        None
373    }
374}
375/// Build a line-start index from a source string.
376///
377/// `line_starts[i]` is the byte offset of line `i` (0-indexed).
378#[allow(missing_docs)]
379pub fn build_line_index(source: &str) -> Vec<usize> {
380    let mut starts = vec![0usize];
381    for (i, &b) in source.as_bytes().iter().enumerate() {
382        if b == b'\n' {
383            starts.push(i + 1);
384        }
385    }
386    starts
387}
388/// Convert a byte offset to a `(line, col)` pair using a pre-built line index.
389///
390/// Both values are 1-indexed.
391#[allow(missing_docs)]
392pub fn offset_to_line_col(line_index: &[usize], offset: usize) -> (usize, usize) {
393    let line = match line_index.binary_search(&offset) {
394        Ok(i) => i,
395        Err(i) => i.saturating_sub(1),
396    };
397    let col = offset - line_index.get(line).copied().unwrap_or(0);
398    (line + 1, col + 1)
399}
400#[cfg(test)]
401mod span_extra_tests {
402    use super::*;
403    use crate::span_util::*;
404    #[test]
405    fn test_source_cursor_basic() {
406        let src = "hello";
407        let mut cursor = SourceCursor::new(src);
408        assert_eq!(cursor.peek(), Some('h'));
409        assert_eq!(cursor.advance(), Some('h'));
410        assert_eq!(cursor.pos(), 1);
411        assert_eq!(cursor.peek(), Some('e'));
412    }
413    #[test]
414    fn test_source_cursor_newline() {
415        let src = "a\nb";
416        let mut cursor = SourceCursor::new(src);
417        cursor.advance();
418        cursor.advance();
419        assert_eq!(cursor.source_pos().line, 2);
420        assert_eq!(cursor.source_pos().col, 1);
421    }
422    #[test]
423    fn test_source_cursor_advance_while() {
424        let src = "   hello";
425        let mut cursor = SourceCursor::new(src);
426        let spaces = cursor.advance_while(|c| c == ' ');
427        assert_eq!(spaces, "   ");
428        assert_eq!(cursor.peek(), Some('h'));
429    }
430    #[test]
431    fn test_source_cursor_eof() {
432        let mut cursor = SourceCursor::new("ab");
433        cursor.advance();
434        cursor.advance();
435        assert!(cursor.is_eof());
436        assert_eq!(cursor.advance(), None);
437    }
438    #[test]
439    fn test_span_registry() {
440        let mut reg = SpanRegistry::new();
441        let span = Span::new(0, 5, 1, 1);
442        reg.register("foo", span.clone());
443        assert!(reg.contains("foo"));
444        assert!(!reg.contains("bar"));
445        assert_eq!(reg.get("foo").expect("key should exist").end, 5);
446        assert_eq!(reg.len(), 1);
447    }
448    #[test]
449    fn test_span_registry_merge() {
450        let mut a = SpanRegistry::new();
451        let mut b = SpanRegistry::new();
452        a.register("x", Span::new(0, 1, 1, 1));
453        b.register("y", Span::new(2, 3, 1, 3));
454        a.merge(b);
455        assert_eq!(a.len(), 2);
456    }
457    #[test]
458    fn test_coalesce_spans() {
459        let spans = vec![
460            Span::new(0, 5, 1, 1),
461            Span::new(3, 8, 1, 4),
462            Span::new(20, 25, 2, 1),
463        ];
464        let coalesced = coalesce_spans(&spans);
465        assert_eq!(coalesced.len(), 2);
466        assert_eq!(coalesced[0].end, 8);
467        assert_eq!(coalesced[1].start, 20);
468    }
469    #[test]
470    fn test_span_gap() {
471        let a = Span::new(0, 5, 1, 1);
472        let b = Span::new(10, 15, 1, 11);
473        let gap = span_gap(&a, &b).expect("span should be present");
474        assert_eq!(gap.start, 5);
475        assert_eq!(gap.end, 10);
476    }
477    #[test]
478    fn test_span_gap_overlap() {
479        let a = Span::new(0, 10, 1, 1);
480        let b = Span::new(5, 15, 1, 6);
481        assert!(span_gap(&a, &b).is_none());
482    }
483    #[test]
484    fn test_build_line_index() {
485        let src = "abc\ndef\nghi";
486        let idx = build_line_index(src);
487        assert_eq!(idx, vec![0, 4, 8]);
488    }
489    #[test]
490    fn test_offset_to_line_col() {
491        let src = "abc\ndef\nghi";
492        let idx = build_line_index(src);
493        assert_eq!(offset_to_line_col(&idx, 0), (1, 1));
494        assert_eq!(offset_to_line_col(&idx, 4), (2, 1));
495        assert_eq!(offset_to_line_col(&idx, 5), (2, 2));
496    }
497    #[test]
498    fn test_sort_spans() {
499        let mut spans = vec![
500            Span::new(10, 15, 1, 11),
501            Span::new(0, 5, 1, 1),
502            Span::new(5, 10, 1, 6),
503        ];
504        sort_spans(&mut spans);
505        assert_eq!(spans[0].start, 0);
506        assert_eq!(spans[1].start, 5);
507        assert_eq!(spans[2].start, 10);
508    }
509    #[test]
510    fn test_span_before_other() {
511        let a = Span::new(0, 5, 1, 1);
512        let b = Span::new(10, 15, 1, 11);
513        assert!(span_before_other(&a, &b));
514        assert!(!span_before_other(&b, &a));
515    }
516}
517/// Highlight a span in source text using a caret line.
518///
519/// Returns a two-line string: the source line, then the caret annotation.
520#[allow(dead_code)]
521#[allow(missing_docs)]
522pub fn highlight_span(source: &str, span: &Span) -> String {
523    let line = extract_line(source, span);
524    let caret = span_caret(span.column, span_len(span));
525    format!("{}\n{}", line, caret)
526}
527/// A zero-length span at byte offset `pos` with the given line/col.
528#[allow(dead_code)]
529#[allow(missing_docs)]
530pub fn zero_span(pos: usize, line: usize, col: usize) -> Span {
531    Span::new(pos, pos, line, col)
532}
533/// Check whether a byte offset is within the bounds of a source string.
534#[allow(dead_code)]
535#[allow(missing_docs)]
536pub fn is_valid_offset(source: &str, offset: usize) -> bool {
537    offset <= source.len()
538}
539/// Count the lines covered by a span.
540///
541/// Returns 1 if the span is on a single line, more if it spans newlines.
542#[allow(dead_code)]
543#[allow(missing_docs)]
544pub fn span_line_count(source: &str, span: &Span) -> usize {
545    let text = extract_span(source, span);
546    text.bytes().filter(|&b| b == b'\n').count() + 1
547}
548/// Return the column at which the span ends (exclusive).
549#[allow(dead_code)]
550#[allow(missing_docs)]
551pub fn span_end_col(span: &Span) -> usize {
552    span.column + span_len(span)
553}
554/// Build an `IndexVec` of all span starts from a list of spans.
555#[allow(dead_code)]
556#[allow(missing_docs)]
557pub fn span_start_offsets(spans: &[Span]) -> Vec<usize> {
558    spans.iter().map(|s| s.start).collect()
559}
560/// Find the span that contains byte `offset`, if any.
561#[allow(dead_code)]
562#[allow(missing_docs)]
563pub fn find_span_at(spans: &[Span], offset: usize) -> Option<&Span> {
564    spans.iter().find(|s| span_contains(s, offset))
565}
566/// Partition spans into two groups: those entirely within `region`, and those outside.
567#[allow(dead_code)]
568#[allow(missing_docs)]
569pub fn partition_spans_by_region<'a>(
570    spans: &'a [Span],
571    region: &Span,
572) -> (Vec<&'a Span>, Vec<&'a Span>) {
573    let inside = spans
574        .iter()
575        .filter(|s| span_contains_span(region, s))
576        .collect();
577    let outside = spans
578        .iter()
579        .filter(|s| !span_contains_span(region, s))
580        .collect();
581    (inside, outside)
582}
583#[cfg(test)]
584mod diag_span_tests {
585    use super::*;
586    use crate::span_util::*;
587    #[test]
588    fn test_diagnostic_span_error() {
589        let span = dummy_span();
590        let d = DiagnosticSpan::error(span, "test error");
591        assert!(d.severity.is_error());
592        assert!(d.format_short().contains("error"));
593    }
594    #[test]
595    fn test_diagnostic_span_warning() {
596        let span = dummy_span();
597        let d = DiagnosticSpan::warning(span, "test warning");
598        assert_eq!(d.severity.label(), "warning");
599    }
600    #[test]
601    fn test_diagnostic_set_count() {
602        let mut set = DiagnosticSet::new();
603        set.add_error(dummy_span(), "e1");
604        set.add_error(dummy_span(), "e2");
605        set.add_warning(dummy_span(), "w1");
606        assert_eq!(set.count_severity(&SpanSeverity::Error), 2);
607        assert_eq!(set.count_severity(&SpanSeverity::Warning), 1);
608        assert_eq!(set.len(), 3);
609        assert!(set.has_errors());
610    }
611    #[test]
612    fn test_diagnostic_set_no_errors() {
613        let mut set = DiagnosticSet::new();
614        set.add_info(dummy_span(), "info");
615        assert!(!set.has_errors());
616    }
617    #[test]
618    fn test_diagnostic_set_merge() {
619        let mut a = DiagnosticSet::new();
620        a.add_error(dummy_span(), "e1");
621        let mut b = DiagnosticSet::new();
622        b.add_warning(dummy_span(), "w1");
623        a.merge(b);
624        assert_eq!(a.len(), 2);
625    }
626    #[test]
627    fn test_diagnostic_set_clear() {
628        let mut set = DiagnosticSet::new();
629        set.add_error(dummy_span(), "e");
630        set.clear();
631        assert!(set.is_empty());
632    }
633    #[test]
634    fn test_highlight_span() {
635        let src = "hello world";
636        let span = Span::new(6, 11, 1, 7);
637        let h = highlight_span(src, &span);
638        assert!(h.contains("hello world"));
639        assert!(h.contains('^'));
640    }
641    #[test]
642    fn test_zero_span() {
643        let z = zero_span(10, 2, 3);
644        assert_eq!(z.start, 10);
645        assert_eq!(z.end, 10);
646        assert!(span_len(&z) == 0);
647    }
648    #[test]
649    fn test_span_line_count() {
650        let src = "ab\ncd\nef";
651        let span = Span::new(0, 8, 1, 1);
652        assert_eq!(span_line_count(src, &span), 3);
653    }
654    #[test]
655    fn test_span_diff() {
656        let old = Span::new(0, 5, 1, 1);
657        let new = Span::new(0, 8, 1, 1);
658        let diff = SpanDiff::compute(old, new);
659        assert!(diff.grew());
660        assert!(!diff.shrank());
661        assert!(!diff.unchanged());
662    }
663    #[test]
664    fn test_span_map() {
665        let mut m: SpanMap<&str> = SpanMap::new();
666        m.insert(5, "hello");
667        m.insert(10, "world");
668        assert_eq!(m.get(5), Some(&"hello"));
669        assert_eq!(m.get(99), None);
670        assert_eq!(m.len(), 2);
671    }
672    #[test]
673    fn test_find_span_at() {
674        let spans = vec![Span::new(0, 5, 1, 1), Span::new(10, 15, 1, 11)];
675        let found = find_span_at(&spans, 3);
676        assert!(found.is_some());
677        let not_found = find_span_at(&spans, 7);
678        assert!(not_found.is_none());
679    }
680    #[test]
681    fn test_span_end_col() {
682        let span = Span::new(0, 5, 1, 1);
683        assert_eq!(span_end_col(&span), 6);
684    }
685    #[test]
686    fn test_is_valid_offset() {
687        let src = "hello";
688        assert!(is_valid_offset(src, 5));
689        assert!(!is_valid_offset(src, 6));
690    }
691}
692/// Convert a byte column offset within a line to a UTF-16 code-unit count.
693///
694/// `line_text` is the full text of the line; `byte_col` is 0-indexed from line start.
695#[allow(dead_code)]
696#[allow(missing_docs)]
697pub fn byte_col_to_utf16(line_text: &str, byte_col: usize) -> usize {
698    let prefix = line_text.get(..byte_col).unwrap_or(line_text);
699    prefix.chars().map(|c| c.len_utf16()).sum()
700}
701/// Convert a UTF-16 code-unit count within a line to a byte offset.
702///
703/// Returns the byte offset at which the UTF-16 unit `utf16_col` falls.
704#[allow(dead_code)]
705#[allow(missing_docs)]
706pub fn utf16_col_to_byte(line_text: &str, utf16_col: usize) -> usize {
707    let mut remaining = utf16_col;
708    let mut byte_offset = 0;
709    for ch in line_text.chars() {
710        if remaining == 0 {
711            break;
712        }
713        let units = ch.len_utf16();
714        if remaining < units {
715            break;
716        }
717        remaining -= units;
718        byte_offset += ch.len_utf8();
719    }
720    byte_offset
721}
722/// Compute the byte offset of a `(line, col)` position in `source`,
723/// where both `line` and `col` are 1-indexed.
724#[allow(dead_code)]
725#[allow(missing_docs)]
726pub fn line_col_to_offset(source: &str, line: usize, col: usize) -> Option<usize> {
727    let mut cur_line = 1usize;
728    let mut cur_col = 1usize;
729    let mut pos = 0usize;
730    for ch in source.chars() {
731        if cur_line == line && cur_col == col {
732            return Some(pos);
733        }
734        if ch == '\n' {
735            cur_line += 1;
736            cur_col = 1;
737        } else {
738            cur_col += 1;
739        }
740        pos += ch.len_utf8();
741    }
742    if cur_line == line && cur_col == col {
743        Some(pos)
744    } else {
745        None
746    }
747}
748/// Apply a byte delta to all spans in a list (e.g., after an edit).
749#[allow(dead_code)]
750#[allow(missing_docs)]
751pub fn shift_spans(spans: &mut [Span], edit_start: usize, delta: i64) {
752    for span in spans.iter_mut() {
753        if span.start >= edit_start {
754            span.start = (span.start as i64 + delta).max(0) as usize;
755            span.end = (span.end as i64 + delta).max(span.start as i64) as usize;
756        } else if span.end > edit_start {
757            span.end = edit_start;
758        }
759    }
760}
761/// Count the number of Unicode scalar values in a span of source.
762#[allow(dead_code)]
763#[allow(missing_docs)]
764pub fn count_chars(source: &str, span: &Span) -> usize {
765    extract_span(source, span).chars().count()
766}
767/// Split a span at a byte offset within it, returning two adjacent spans.
768#[allow(dead_code)]
769#[allow(missing_docs)]
770pub fn split_span(span: &Span, split_at: usize) -> (Span, Span) {
771    assert!(
772        split_at >= span.start && split_at <= span.end,
773        "split_at out of range"
774    );
775    let left = Span::new(span.start, split_at, span.line, span.column);
776    let right_col = span.column + (split_at - span.start);
777    let right = Span::new(split_at, span.end, span.line, right_col);
778    (left, right)
779}
780/// Format a `Span` as `"file:line:col"` using an optional file name.
781#[allow(dead_code)]
782#[allow(missing_docs)]
783pub fn span_to_diagnostic_string(file: &str, span: &Span) -> String {
784    format!("{}:{}:{}", file, span.line, span.column)
785}
786/// Format a multi-line diagnostic snippet.
787#[allow(dead_code)]
788#[allow(missing_docs)]
789pub fn format_diagnostic(source: &str, span: &Span, label: &str) -> String {
790    let line = extract_line(source, span);
791    let caret = span_caret(span.column, span_len(span));
792    format!("{}\n{}\n{}", line, caret, label)
793}
794/// Convert a `Span` to a human-readable summary string for debugging.
795#[allow(dead_code)]
796#[allow(missing_docs)]
797pub fn span_debug_str(span: &Span) -> String {
798    format!(
799        "Span {{ start: {}, end: {}, line: {}, col: {} }}",
800        span.start, span.end, span.line, span.column
801    )
802}
803/// Return `true` if two spans are exactly adjacent.
804#[allow(dead_code)]
805#[allow(missing_docs)]
806pub fn spans_adjacent(a: &Span, b: &Span) -> bool {
807    a.end == b.start || b.end == a.start
808}
809/// Compute the center byte offset of a span.
810#[allow(dead_code)]
811#[allow(missing_docs)]
812pub fn span_center(span: &Span) -> usize {
813    span.start + span_len(span) / 2
814}
815/// Check whether a span is a strict subspan of another.
816#[allow(dead_code)]
817#[allow(missing_docs)]
818pub fn span_strictly_contains(outer: &Span, inner: &Span) -> bool {
819    outer.start <= inner.start
820        && inner.end <= outer.end
821        && !(outer.start == inner.start && outer.end == inner.end)
822}
823/// Sort spans by their end offset (ascending).
824#[allow(dead_code)]
825#[allow(missing_docs)]
826pub fn sort_spans_by_end(spans: &mut [Span]) {
827    spans.sort_by(|a, b| a.end.cmp(&b.end).then(a.start.cmp(&b.start)));
828}
829/// Sort spans in reverse order (largest start offset first).
830#[allow(dead_code)]
831#[allow(missing_docs)]
832pub fn sort_spans_reverse(spans: &mut [Span]) {
833    spans.sort_by(|a, b| b.start.cmp(&a.start));
834}
835/// Return `true` if `spans` are sorted by start offset.
836#[allow(dead_code)]
837#[allow(missing_docs)]
838pub fn spans_are_sorted(spans: &[Span]) -> bool {
839    spans.windows(2).all(|w| w[0].start <= w[1].start)
840}
841/// Deduplicate a sorted list of spans (remove exact duplicates).
842#[allow(dead_code)]
843#[allow(missing_docs)]
844pub fn dedup_spans(spans: &mut Vec<Span>) {
845    spans.dedup_by(|a, b| a.start == b.start && a.end == b.end);
846}
847/// Return all spans that do NOT overlap with `exclude`.
848#[allow(dead_code)]
849#[allow(missing_docs)]
850pub fn filter_non_overlapping<'a>(spans: &'a [Span], exclude: &Span) -> Vec<&'a Span> {
851    spans
852        .iter()
853        .filter(|s| !spans_overlap(s, exclude))
854        .collect()
855}
856/// Total ordering comparison for spans: by start, then by end.
857#[allow(dead_code)]
858#[allow(missing_docs)]
859pub fn span_cmp(a: &Span, b: &Span) -> std::cmp::Ordering {
860    a.start.cmp(&b.start).then(a.end.cmp(&b.end))
861}
862/// `true` if `a` ends at or before `b` starts (non-overlapping).
863#[allow(dead_code)]
864#[allow(missing_docs)]
865pub fn span_precedes(a: &Span, b: &Span) -> bool {
866    a.end <= b.start
867}
868/// `true` if `a` and `b` are the same byte range.
869#[allow(dead_code)]
870#[allow(missing_docs)]
871pub fn span_eq_range(a: &Span, b: &Span) -> bool {
872    a.start == b.start && a.end == b.end
873}
874/// Compute the minimum distance (in bytes) between two non-overlapping spans.
875#[allow(dead_code)]
876#[allow(missing_docs)]
877pub fn span_distance(a: &Span, b: &Span) -> usize {
878    if spans_overlap(a, b) {
879        0
880    } else if a.end <= b.start {
881        b.start - a.end
882    } else {
883        a.start - b.end
884    }
885}
886/// Choose the span that starts earlier.
887#[allow(dead_code)]
888#[allow(missing_docs)]
889pub fn span_earlier<'a>(a: &'a Span, b: &'a Span) -> &'a Span {
890    if a.start <= b.start {
891        a
892    } else {
893        b
894    }
895}
896/// Choose the span that ends later.
897#[allow(dead_code)]
898#[allow(missing_docs)]
899pub fn span_later<'a>(a: &'a Span, b: &'a Span) -> &'a Span {
900    if a.end >= b.end {
901        a
902    } else {
903        b
904    }
905}
906/// Extract a "window" of lines around a span for error reporting.
907#[allow(dead_code)]
908#[allow(missing_docs)]
909pub fn extract_context_window(
910    source: &str,
911    span: &Span,
912    context_lines: usize,
913) -> Vec<(usize, String)> {
914    let lines: Vec<&str> = source.split('\n').collect();
915    let span_line = span.line.saturating_sub(1);
916    let first = span_line.saturating_sub(context_lines);
917    let last = (span_line + context_lines + 1).min(lines.len());
918    lines[first..last]
919        .iter()
920        .enumerate()
921        .map(|(i, &l)| (first + i + 1, l.to_string()))
922        .collect()
923}
924/// Format a context window as a human-readable string with line numbers.
925#[allow(dead_code)]
926#[allow(missing_docs)]
927pub fn format_context_window(window: &[(usize, String)], highlight_line: usize) -> String {
928    let mut out = String::new();
929    for (ln, text) in window {
930        let marker = if *ln == highlight_line { ">" } else { " " };
931        out.push_str(&format!("{} {:4} | {}\n", marker, ln, text));
932    }
933    out
934}
935#[cfg(test)]
936mod extended_span_tests {
937    use super::*;
938    use crate::span_util::*;
939    #[test]
940    fn test_file_id_basic() {
941        let id = FileId::new(42);
942        assert_eq!(id.raw(), 42);
943        assert_eq!(id.to_string(), "file#42");
944    }
945    #[test]
946    fn test_file_registry_register() {
947        let mut reg = FileRegistry::new();
948        let id = reg.register("foo.lean", "def x := 1");
949        assert_eq!(reg.source(id), Some("def x := 1"));
950        assert_eq!(reg.path(id), Some("foo.lean"));
951    }
952    #[test]
953    fn test_file_registry_extract() {
954        let mut reg = FileRegistry::new();
955        let id = reg.register("a.lean", "hello world");
956        let span = Span::new(6, 11, 1, 7);
957        let fspan = FileSpan::new(id, span);
958        assert_eq!(reg.extract(&fspan), "world");
959    }
960    #[test]
961    fn test_file_span_merge() {
962        let id = FileId::new(1);
963        let a = FileSpan::new(id, Span::new(0, 5, 1, 1));
964        let b = FileSpan::new(id, Span::new(3, 8, 1, 4));
965        let merged = a.merge_with(&b);
966        assert_eq!(merged.span.start, 0);
967        assert_eq!(merged.span.end, 8);
968    }
969    #[test]
970    fn test_line_col_to_offset_basic() {
971        let src = "abc\ndef\nghi";
972        assert_eq!(line_col_to_offset(src, 1, 1), Some(0));
973        assert_eq!(line_col_to_offset(src, 2, 1), Some(4));
974    }
975    #[test]
976    fn test_shift_spans() {
977        let mut spans = vec![Span::new(0, 5, 1, 1), Span::new(10, 15, 1, 11)];
978        shift_spans(&mut spans, 8, 3);
979        assert_eq!(spans[0].start, 0);
980        assert_eq!(spans[1].start, 13);
981    }
982    #[test]
983    fn test_split_span_basic() {
984        let span = Span::new(0, 10, 1, 1);
985        let (left, right) = split_span(&span, 5);
986        assert_eq!(left.end, 5);
987        assert_eq!(right.start, 5);
988    }
989    #[test]
990    fn test_span_strictly_contains() {
991        let outer = Span::new(0, 10, 1, 1);
992        let inner = Span::new(2, 8, 1, 3);
993        assert!(span_strictly_contains(&outer, &inner));
994    }
995    #[test]
996    fn test_spans_adjacent() {
997        let a = Span::new(0, 5, 1, 1);
998        let b = Span::new(5, 10, 1, 6);
999        assert!(spans_adjacent(&a, &b));
1000    }
1001    #[test]
1002    fn test_annotated_span() {
1003        let span = Span::new(0, 5, 1, 1);
1004        let a = AnnotatedSpan::new(span, "test");
1005        assert_eq!(a.annotation, "test");
1006        assert_eq!(a.len(), 5);
1007    }
1008    #[test]
1009    fn test_span_annotations_at_offset() {
1010        let mut anns = SpanAnnotations::new();
1011        anns.annotate(Span::new(0, 10, 1, 1), "first");
1012        anns.annotate(Span::new(5, 15, 1, 6), "second");
1013        let at7 = anns.at_offset(7);
1014        assert_eq!(at7.len(), 2);
1015    }
1016    #[test]
1017    fn test_incremental_tracker() {
1018        let mut tracker = IncrementalSpanTracker::new();
1019        tracker.track(Span::new(0, 5, 1, 1));
1020        tracker.track(Span::new(10, 15, 1, 11));
1021        tracker.apply_edit(8, 3);
1022        assert_eq!(tracker.spans()[1].start, 13);
1023        assert_eq!(tracker.edit_count(), 1);
1024    }
1025    #[test]
1026    fn test_span_origin() {
1027        assert_eq!(SpanOrigin::UserSource.kind_str(), "user");
1028        assert_eq!(SpanOrigin::Synthetic.kind_str(), "synthetic");
1029    }
1030    #[test]
1031    fn test_span_stats() {
1032        let spans = vec![Span::new(0, 5, 1, 1), Span::new(10, 20, 2, 1)];
1033        let stats = SpanStats::compute(&spans);
1034        assert_eq!(stats.count, 2);
1035        assert_eq!(stats.total_len, 15);
1036        assert_eq!(stats.avg_len(), 7);
1037    }
1038    #[test]
1039    fn test_span_chain() {
1040        let mut chain = SpanChain::new();
1041        chain.push(Span::new(0, 5, 1, 1));
1042        chain.push(Span::new(6, 10, 1, 7));
1043        let total = chain.total_span().expect("span should be present");
1044        assert_eq!(total.start, 0);
1045        assert_eq!(total.end, 10);
1046    }
1047    #[test]
1048    fn test_span_cmp() {
1049        let a = Span::new(0, 5, 1, 1);
1050        let b = Span::new(5, 10, 1, 6);
1051        assert_eq!(span_cmp(&a, &b), std::cmp::Ordering::Less);
1052    }
1053    #[test]
1054    fn test_span_distance_nonoverlap() {
1055        let a = Span::new(0, 5, 1, 1);
1056        let b = Span::new(10, 15, 1, 11);
1057        assert_eq!(span_distance(&a, &b), 5);
1058    }
1059    #[test]
1060    fn test_span_earlier_later() {
1061        let a = Span::new(0, 5, 1, 1);
1062        let b = Span::new(3, 12, 1, 4);
1063        assert_eq!(span_earlier(&a, &b).start, 0);
1064        assert_eq!(span_later(&a, &b).end, 12);
1065    }
1066    #[test]
1067    fn test_count_chars_unicode() {
1068        let src = "αβγδ";
1069        let span = Span::new(0, src.len(), 1, 1);
1070        assert_eq!(count_chars(src, &span), 4);
1071    }
1072    #[test]
1073    fn test_extract_context_window() {
1074        let src = "line1\nline2\nline3\nline4\nline5";
1075        let span = Span::new(12, 17, 3, 1);
1076        let window = extract_context_window(src, &span, 1);
1077        assert_eq!(window.len(), 3);
1078        assert_eq!(window[1].0, 3);
1079    }
1080    #[test]
1081    fn test_dedup_spans() {
1082        let mut spans = vec![
1083            Span::new(0, 5, 1, 1),
1084            Span::new(0, 5, 1, 1),
1085            Span::new(6, 10, 1, 7),
1086        ];
1087        dedup_spans(&mut spans);
1088        assert_eq!(spans.len(), 2);
1089    }
1090    #[test]
1091    fn test_sort_spans_reverse() {
1092        let mut spans = vec![Span::new(0, 5, 1, 1), Span::new(10, 15, 1, 11)];
1093        sort_spans_reverse(&mut spans);
1094        assert_eq!(spans[0].start, 10);
1095    }
1096    #[test]
1097    fn test_byte_col_to_utf16_ascii() {
1098        let line = "hello world";
1099        assert_eq!(byte_col_to_utf16(line, 5), 5);
1100    }
1101    #[test]
1102    fn test_span_debug_str() {
1103        let span = Span::new(1, 5, 2, 3);
1104        let s = span_debug_str(&span);
1105        assert!(s.contains("start: 1"));
1106    }
1107    #[test]
1108    fn test_span_center() {
1109        let span = Span::new(2, 8, 1, 3);
1110        assert_eq!(span_center(&span), 5);
1111    }
1112    #[test]
1113    fn test_sort_spans_by_end() {
1114        let mut spans = vec![Span::new(5, 15, 1, 6), Span::new(0, 8, 1, 1)];
1115        sort_spans_by_end(&mut spans);
1116        assert_eq!(spans[0].end, 8);
1117    }
1118}
1119/// Check if spans are sorted by start offset.
1120#[allow(dead_code)]
1121#[allow(missing_docs)]
1122pub fn spans_sorted_check(spans: &[Span]) -> bool {
1123    spans.windows(2).all(|w| w[0].start <= w[1].start)
1124}
1125/// Return the total byte coverage of a set of spans (counting overlaps once).
1126#[allow(dead_code)]
1127#[allow(missing_docs)]
1128pub fn total_coverage(spans: &[Span]) -> usize {
1129    if spans.is_empty() {
1130        return 0;
1131    }
1132    let mut sorted = spans.to_vec();
1133    sort_spans(&mut sorted);
1134    let coalesced = coalesce_spans(&sorted);
1135    coalesced.iter().map(span_len).sum()
1136}
1137/// Given a list of prioritized spans, return the one with the highest priority at .
1138#[allow(dead_code)]
1139#[allow(missing_docs)]
1140pub fn highest_priority_at(spans: &[PrioritizedSpan], offset: usize) -> Option<&PrioritizedSpan> {
1141    spans
1142        .iter()
1143        .filter(|ps| span_contains(&ps.span, offset))
1144        .max_by_key(|ps| ps.priority)
1145}
1146#[cfg(test)]
1147mod coverage_extra_tests {
1148    use super::*;
1149    use crate::span_util::*;
1150    #[test]
1151    fn test_total_coverage_no_overlap() {
1152        let spans = vec![Span::new(0, 5, 1, 1), Span::new(10, 15, 1, 11)];
1153        assert_eq!(total_coverage(&spans), 10);
1154    }
1155    #[test]
1156    fn test_total_coverage_overlap() {
1157        let spans = vec![Span::new(0, 10, 1, 1), Span::new(5, 15, 1, 6)];
1158        assert_eq!(total_coverage(&spans), 15);
1159    }
1160    #[test]
1161    fn test_total_coverage_empty() {
1162        assert_eq!(total_coverage(&[]), 0);
1163    }
1164    #[test]
1165    fn test_spans_sorted_check_true() {
1166        let sorted = vec![Span::new(0, 5, 1, 1), Span::new(6, 10, 1, 7)];
1167        assert!(spans_sorted_check(&sorted));
1168    }
1169    #[test]
1170    fn test_spans_sorted_check_false() {
1171        let unsorted = vec![Span::new(6, 10, 1, 7), Span::new(0, 5, 1, 1)];
1172        assert!(!spans_sorted_check(&unsorted));
1173    }
1174    #[test]
1175    fn test_highest_priority_at() {
1176        let spans = vec![
1177            PrioritizedSpan::new(Span::new(0, 10, 1, 1), 1),
1178            PrioritizedSpan::new(Span::new(3, 7, 1, 4), 5),
1179        ];
1180        let best = highest_priority_at(&spans, 5).expect("span should be present");
1181        assert_eq!(best.priority, 5);
1182    }
1183    #[test]
1184    fn test_highest_priority_at_none() {
1185        let spans = vec![PrioritizedSpan::new(Span::new(0, 5, 1, 1), 1)];
1186        assert!(highest_priority_at(&spans, 10).is_none());
1187    }
1188    #[test]
1189    fn test_span_range() {
1190        let r = SpanRange::new(2, 7);
1191        assert_eq!(r.len(), 5);
1192        assert!(r.contains(4));
1193        assert!(!r.contains(7));
1194    }
1195    #[test]
1196    fn test_padded_span() {
1197        let inner = Span::new(5, 10, 1, 6);
1198        let padded = PaddedSpan::new(inner, 3, 2);
1199        let expanded = padded.expanded(100);
1200        assert_eq!(expanded.start, 2);
1201        assert_eq!(expanded.end, 12);
1202    }
1203    #[test]
1204    fn test_padded_span_clamp() {
1205        let inner = Span::new(0, 5, 1, 1);
1206        let padded = PaddedSpan::new(inner, 10, 100);
1207        let expanded = padded.expanded(8);
1208        assert_eq!(expanded.start, 0);
1209        assert_eq!(expanded.end, 8);
1210    }
1211}