praxis_syntax/span_bridge.rs
1//! Bridge between Praxis spans and rowan's text offsets.
2//!
3//! Praxis `Span`/`BytePos` are the source of truth for diagnostics and never
4//! leak rowan types outward. Internally, rowan indexes the tree with its own
5//! `TextSize`/`TextRange` (also byte offsets into UTF-8). Neither type is local
6//! to this crate, so the orphan rule forbids `From` impls between them; these
7//! free functions are the only place the two meet (ADR-003).
8//!
9//! Prefer the explicit `span_to_range` / `range_to_span` calls at the tree
10//! boundary over implicit conversions, so every crossing is visible.
11
12use praxis_source::{BytePos, Span};
13use rowan::{TextRange, TextSize};
14
15/// Convert a Praxis [`Span`] into a rowan [`TextRange`].
16///
17/// `Span` is never inverted (start ≤ end by construction), so this cannot panic
18/// on `TextRange::new`'s internal assertion.
19#[inline]
20#[must_use]
21pub fn span_to_range(span: Span) -> TextRange {
22 TextRange::new(
23 TextSize::from(span.start().to_u32()),
24 TextSize::from(span.end().to_u32()),
25 )
26}
27
28/// Convert a rowan [`TextRange`] back into a Praxis [`Span`].
29#[inline]
30#[must_use]
31pub fn range_to_span(range: TextRange) -> Span {
32 Span::new(
33 BytePos::from(u32::from(range.start())),
34 BytePos::from(u32::from(range.end())),
35 )
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41
42 #[test]
43 fn span_round_trips_through_textrange() {
44 for (start, end) in [(0, 0), (0, 3), (5, 5), (10, 42)] {
45 let span = Span::new(BytePos::from(start), BytePos::from(end));
46 let range = span_to_range(span);
47 let back = range_to_span(range);
48 assert_eq!(back, span);
49 }
50 }
51
52 #[test]
53 fn empty_span_maps_to_empty_range() {
54 let span = Span::new(BytePos::from(7), BytePos::from(7));
55 let range = span_to_range(span);
56 assert!(range.is_empty());
57 }
58}