1#[non_exhaustive]
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub enum ColumnUnit {
7 Utf8,
9 Char,
11 Utf16,
13}
14
15#[non_exhaustive]
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub struct LineCol {
20 pub line: usize,
22 pub column: usize,
24}
25
26impl LineCol {
27 #[must_use]
30 pub const fn new(line: usize, column: usize) -> Self {
31 Self { line, column }
32 }
33}
34
35#[derive(Clone, Debug, PartialEq, Eq, Hash)]
39struct Wide {
40 at: usize,
41 len: usize,
42 extra_chars: usize,
43 extra_utf16: usize,
44}
45
46#[derive(Clone, Debug, PartialEq, Eq, Hash)]
73pub struct LineIndex {
74 starts: Vec<usize>,
76 wide: Vec<Wide>,
78 len: usize,
79}
80
81impl LineIndex {
82 #[must_use]
84 pub fn new(src: &str) -> Self {
85 let mut starts = vec![0];
86 let mut wide = Vec::new();
87 let (mut extra_chars, mut extra_utf16) = (0usize, 0usize);
88 let mut chars = src.char_indices().peekable();
89 while let Some((at, c)) = chars.next() {
90 let len = c.len_utf8();
91 let next = at.saturating_add(len);
92 match c {
93 '\n' => starts.push(next),
94 '\r' if !matches!(chars.peek(), Some((_, '\n'))) => starts.push(next),
95 _ if len > 1 => {
96 extra_chars = extra_chars.saturating_add(len.saturating_sub(1));
97 extra_utf16 = extra_utf16.saturating_add(len.saturating_sub(c.len_utf16()));
98 wide.push(Wide {
99 at,
100 len,
101 extra_chars,
102 extra_utf16,
103 });
104 }
105 _ => {}
106 }
107 }
108 Self {
109 starts,
110 wide,
111 len: src.len(),
112 }
113 }
114
115 #[must_use]
117 pub fn len(&self) -> usize {
118 self.len
119 }
120
121 #[must_use]
123 pub fn is_empty(&self) -> bool {
124 self.len == 0
125 }
126
127 #[must_use]
130 pub fn line_count(&self) -> usize {
131 self.starts.len()
132 }
133
134 #[must_use]
136 pub fn line_start(&self, line: usize) -> Option<usize> {
137 self.starts.get(line).copied()
138 }
139
140 fn extra_before(&self, offset: usize) -> Option<(usize, usize)> {
144 let n = self.wide.partition_point(|w| w.at < offset);
145 let Some(last) = n.checked_sub(1).and_then(|i| self.wide.get(i)) else {
146 return Some((0, 0));
147 };
148 if offset < last.at.saturating_add(last.len) {
149 return None;
150 }
151 Some((last.extra_chars, last.extra_utf16))
152 }
153
154 #[must_use]
157 pub fn line_col(&self, offset: usize, unit: ColumnUnit) -> Option<LineCol> {
158 if offset > self.len {
159 return None;
160 }
161 let line = self
162 .starts
163 .partition_point(|s| *s <= offset)
164 .checked_sub(1)?;
165 let start = *self.starts.get(line)?;
166 let bytes = offset.checked_sub(start)?;
167 let (at_chars, at_utf16) = self.extra_before(offset)?;
168 let (start_chars, start_utf16) = self.extra_before(start)?;
169 let column = match unit {
170 ColumnUnit::Char => bytes.checked_sub(at_chars.checked_sub(start_chars)?)?,
171 ColumnUnit::Utf16 => bytes.checked_sub(at_utf16.checked_sub(start_utf16)?)?,
172 ColumnUnit::Utf8 => bytes,
173 };
174 Some(LineCol { line, column })
175 }
176
177 #[must_use]
180 pub fn span(&self, span: crate::Span, unit: ColumnUnit) -> Option<(LineCol, LineCol)> {
181 Some((
182 self.line_col(span.start, unit)?,
183 self.line_col(span.end, unit)?,
184 ))
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 #![allow(
191 clippy::unwrap_used,
192 clippy::expect_used,
193 clippy::indexing_slicing,
194 clippy::arithmetic_side_effects
195 )]
196 use super::*;
197
198 fn naive(src: &str, offset: usize, unit: ColumnUnit) -> Option<LineCol> {
200 let before = src.get(..offset)?;
201 let (mut line, mut column) = (0usize, 0usize);
202 let mut prev_cr = false;
203 for c in before.chars() {
204 match c {
205 '\n' if prev_cr => {
206 column = 0;
207 }
208 '\n' => {
209 line += 1;
210 column = 0;
211 }
212 '\r' => {
213 line += 1;
214 column = 0;
215 }
216 _ => {
217 column += match unit {
218 ColumnUnit::Utf8 => c.len_utf8(),
219 ColumnUnit::Char => 1,
220 ColumnUnit::Utf16 => c.len_utf16(),
221 }
222 }
223 }
224 prev_cr = c == '\r';
225 }
226 Some(LineCol { line, column })
227 }
228
229 #[test]
230 fn agrees_with_a_walk_everywhere() {
231 let src = "a\r\nb\rc\nd\u{e9}x\u{1F600}\u{4e2d}\r\n\n\u{e9}";
232 let idx = LineIndex::new(src);
233 assert_eq!(idx.len(), src.len());
234 assert!(!idx.is_empty());
235 assert_eq!(idx.line_count(), 6);
236 for offset in 0..=src.len() + 2 {
237 for unit in [ColumnUnit::Utf8, ColumnUnit::Char, ColumnUnit::Utf16] {
238 let crlf_nl = src.get(offset..).is_some_and(|r| r.starts_with('\n'))
240 && src.get(..offset).is_some_and(|r| r.ends_with('\r'));
241 if crlf_nl {
242 let at = idx.line_col(offset, unit).unwrap();
243 let before = idx.line_col(offset - 1, unit).unwrap();
244 assert_eq!(at.line, before.line);
245 assert_eq!(at.column, before.column + 1);
246 continue;
247 }
248 assert_eq!(
249 idx.line_col(offset, unit),
250 naive(src, offset, unit),
251 "{offset} {unit:?}"
252 );
253 }
254 }
255 assert_eq!(idx.line_start(1), Some(3));
256 assert_eq!(idx.line_start(99), None);
257 let e = src.find('x').unwrap();
258 assert_eq!(
259 idx.span(crate::Span::new(e, e + 1), ColumnUnit::Char),
260 Some((
261 LineCol { line: 3, column: 2 },
262 LineCol { line: 3, column: 3 }
263 ))
264 );
265 assert_eq!(idx.span(crate::Span::new(0, 999), ColumnUnit::Char), None);
266 }
267
268 #[test]
269 fn empty_and_ascii() {
270 let idx = LineIndex::new("");
271 assert!(idx.is_empty());
272 assert_eq!(
273 idx.line_col(0, ColumnUnit::Utf16),
274 Some(LineCol { line: 0, column: 0 })
275 );
276 assert_eq!(idx.line_col(1, ColumnUnit::Utf16), None);
277 let idx = LineIndex::new("ab\n");
278 assert_eq!(
279 idx.line_col(3, ColumnUnit::Char),
280 Some(LineCol { line: 1, column: 0 })
281 );
282 assert_eq!(idx.line_col(usize::MAX, ColumnUnit::Char), None);
283 }
284}