compose_lens/source/
mod.rs1use std::fmt;
4use std::ops::Range;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct SourceId(u32);
12
13impl SourceId {
14 #[must_use]
16 pub const fn new(value: u32) -> Self {
17 Self(value)
18 }
19
20 #[must_use]
22 pub const fn get(self) -> u32 {
23 self.0
24 }
25}
26
27impl fmt::Display for SourceId {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(formatter, "source#{}", self.0)
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct SourceSpan {
36 source_id: SourceId,
37 start: usize,
38 end: usize,
39}
40
41impl SourceSpan {
42 #[must_use]
44 pub const fn new(source_id: SourceId, start: usize, end: usize) -> Option<Self> {
45 if start <= end {
46 Some(Self { source_id, start, end })
47 } else {
48 None
49 }
50 }
51
52 pub(crate) const fn from_valid_offsets(source_id: SourceId, start: usize, end: usize) -> Self {
53 Self { source_id, start, end }
54 }
55
56 #[must_use]
58 pub const fn source_id(self) -> SourceId {
59 self.source_id
60 }
61
62 #[must_use]
64 pub const fn start(self) -> usize {
65 self.start
66 }
67
68 #[must_use]
70 pub const fn end(self) -> usize {
71 self.end
72 }
73
74 #[must_use]
76 pub fn range(self) -> Range<usize> {
77 self.start..self.end
78 }
79
80 #[must_use]
82 pub const fn len(self) -> usize {
83 self.end - self.start
84 }
85
86 #[must_use]
88 pub const fn is_empty(self) -> bool {
89 self.start == self.end
90 }
91
92 #[must_use]
94 pub const fn contains(self, byte_offset: usize) -> bool {
95 self.start <= byte_offset && byte_offset < self.end
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub struct LineColumn {
102 line: usize,
103 column: usize,
104}
105
106impl LineColumn {
107 #[must_use]
109 pub const fn line(self) -> usize {
110 self.line
111 }
112
113 #[must_use]
115 pub const fn column(self) -> usize {
116 self.column
117 }
118}
119
120#[must_use]
124pub fn line_column(text: &str, byte_offset: usize) -> Option<LineColumn> {
125 if byte_offset > text.len() || !text.is_char_boundary(byte_offset) {
126 return None;
127 }
128
129 let prefix = &text[..byte_offset];
130 let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1;
131 let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
132 let column = text[line_start..byte_offset].chars().count() + 1;
133
134 Some(LineColumn { line, column })
135}
136
137#[cfg(test)]
138mod tests {
139 use super::{SourceId, SourceSpan, line_column};
140
141 #[test]
142 fn rejects_a_reversed_span() {
143 assert_eq!(SourceSpan::new(SourceId::new(1), 4, 3), None);
144 }
145
146 #[test]
147 fn calculates_unicode_scalar_columns() {
148 let text = "name: Käfer\nimage: demo\n";
149 let position = line_column(text, "name: Kä".len());
150
151 assert_eq!(position.map(super::LineColumn::line), Some(1));
152 assert_eq!(position.map(super::LineColumn::column), Some(9));
153 }
154}