1use std::{fmt, path::Path, sync::Arc};
2
3#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
5#[repr(transparent)]
6pub struct SourceId(u32);
7
8impl SourceId {
9 #[must_use]
11 pub const fn new(value: u32) -> Self {
12 Self(value)
13 }
14
15 #[must_use]
17 pub const fn get(self) -> u32 {
18 self.0
19 }
20}
21
22impl From<u32> for SourceId {
23 fn from(value: u32) -> Self {
24 Self::new(value)
25 }
26}
27#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub struct SourceIdentity {
33 source_id: SourceId,
34 path: Arc<Path>,
35}
36
37impl SourceIdentity {
38 pub(crate) fn new(source_id: SourceId, path: Arc<Path>) -> Self {
39 Self { source_id, path }
40 }
41
42 #[must_use]
43 pub const fn source_id(&self) -> SourceId {
44 self.source_id
45 }
46
47 #[must_use]
48 pub fn path(&self) -> &Path {
49 &self.path
50 }
51}
52
53#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
55#[repr(transparent)]
56pub struct Utf16Pos(usize);
57
58impl Utf16Pos {
59 pub const ZERO: Self = Self(0);
61
62 #[must_use]
64 pub const fn new(offset: usize) -> Self {
65 Self(offset)
66 }
67
68 #[must_use]
70 pub const fn get(self) -> usize {
71 self.0
72 }
73}
74
75impl From<usize> for Utf16Pos {
76 fn from(offset: usize) -> Self {
77 Self::new(offset)
78 }
79}
80
81#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
83pub struct TextRange {
84 start: Utf16Pos,
85 end: Utf16Pos,
86}
87
88impl TextRange {
89 pub const fn new(start: Utf16Pos, end: Utf16Pos) -> Result<Self, SourcePositionError> {
91 if start.get() > end.get() {
92 return Err(SourcePositionError::RangeStartAfterEnd { start, end });
93 }
94
95 Ok(Self { start, end })
96 }
97
98 #[must_use]
100 pub const fn start(self) -> Utf16Pos {
101 self.start
102 }
103
104 #[must_use]
106 pub const fn end(self) -> Utf16Pos {
107 self.end
108 }
109
110 #[must_use]
112 pub const fn is_empty(self) -> bool {
113 self.start.get() == self.end.get()
114 }
115
116 #[must_use]
118 pub const fn len(self) -> usize {
119 self.end.get() - self.start.get()
120 }
121}
122
123#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
125pub enum ScriptKind {
126 JavaScript,
127 JavaScriptReact,
128 TypeScript,
129 TypeScriptReact,
130 Json,
131}
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub enum SourcePositionError {
139 ByteOffsetOutOfBounds { offset: usize, len: usize },
140 ByteOffsetInsideCodePoint { offset: usize },
141 Utf16PositionOutOfBounds { position: Utf16Pos, len: Utf16Pos },
142 Utf16PositionInsideSurrogatePair { position: Utf16Pos },
143 RangeStartAfterEnd { start: Utf16Pos, end: Utf16Pos },
144}
145
146impl fmt::Display for SourcePositionError {
147 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148 match *self {
149 Self::ByteOffsetOutOfBounds { offset, len } => {
150 write!(
151 formatter,
152 "byte offset {offset} exceeds source length {len}"
153 )
154 }
155 Self::ByteOffsetInsideCodePoint { offset } => {
156 write!(formatter, "byte offset {offset} splits a UTF-8 code point")
157 }
158 Self::Utf16PositionOutOfBounds { position, len } => write!(
159 formatter,
160 "UTF-16 position {} exceeds source length {}",
161 position.get(),
162 len.get()
163 ),
164 Self::Utf16PositionInsideSurrogatePair { position } => write!(
165 formatter,
166 "UTF-16 position {} splits a surrogate pair",
167 position.get()
168 ),
169 Self::RangeStartAfterEnd { start, end } => write!(
170 formatter,
171 "range start {} follows range end {}",
172 start.get(),
173 end.get()
174 ),
175 }
176 }
177}
178
179impl std::error::Error for SourcePositionError {}
180
181#[derive(Clone, Copy, Debug)]
182struct BoundaryCheckpoint {
183 byte: usize,
184 utf16: Utf16Pos,
185}
186
187#[derive(Clone, Debug)]
193pub struct SourceText {
194 text: Arc<str>,
195 checkpoints: Arc<[BoundaryCheckpoint]>,
196 line_starts: Arc<[Utf16Pos]>,
197 utf16_len: Utf16Pos,
198}
199
200impl SourceText {
201 #[must_use]
203 pub fn new(text: impl Into<Arc<str>>) -> Self {
204 Self::from_arc(text.into())
205 }
206
207 #[must_use]
209 pub fn from_arc(text: Arc<str>) -> Self {
210 let mut checkpoints = vec![BoundaryCheckpoint {
211 byte: 0,
212 utf16: Utf16Pos::ZERO,
213 }];
214 let mut line_starts = vec![Utf16Pos::ZERO];
215 let mut utf16_offset = 0;
216 let mut characters = text.char_indices().peekable();
217
218 while let Some((byte_start, character)) = characters.next() {
219 utf16_offset += character.len_utf16();
220
221 if !character.is_ascii() {
222 checkpoints.push(BoundaryCheckpoint {
223 byte: byte_start + character.len_utf8(),
224 utf16: Utf16Pos::new(utf16_offset),
225 });
226 }
227
228 let ends_line = character == '\n'
229 || (character == '\r' && !matches!(characters.peek(), Some(&(_, '\n'))))
230 || character == '\u{2028}'
231 || character == '\u{2029}';
232 if ends_line {
233 line_starts.push(Utf16Pos::new(utf16_offset));
234 }
235 }
236
237 Self {
238 text,
239 checkpoints: Arc::from(checkpoints),
240 line_starts: Arc::from(line_starts),
241 utf16_len: Utf16Pos::new(utf16_offset),
242 }
243 }
244
245 #[must_use]
247 pub fn as_str(&self) -> &str {
248 self.text.as_ref()
249 }
250
251 #[must_use]
253 pub const fn len_utf16(&self) -> Utf16Pos {
254 self.utf16_len
255 }
256
257 #[must_use]
259 pub fn is_empty(&self) -> bool {
260 self.text.is_empty()
261 }
262
263 pub fn byte_to_utf16(&self, byte_offset: usize) -> Result<Utf16Pos, SourcePositionError> {
265 if byte_offset > self.text.len() {
266 return Err(SourcePositionError::ByteOffsetOutOfBounds {
267 offset: byte_offset,
268 len: self.text.len(),
269 });
270 }
271 if !self.text.is_char_boundary(byte_offset) {
272 return Err(SourcePositionError::ByteOffsetInsideCodePoint {
273 offset: byte_offset,
274 });
275 }
276
277 let checkpoint_index = self
278 .checkpoints
279 .partition_point(|checkpoint| checkpoint.byte <= byte_offset)
280 .saturating_sub(1);
281 let checkpoint = &self.checkpoints[checkpoint_index];
282 Ok(Utf16Pos::new(
283 checkpoint.utf16.get() + (byte_offset - checkpoint.byte),
284 ))
285 }
286
287 pub fn utf16_to_byte(&self, position: Utf16Pos) -> Result<usize, SourcePositionError> {
289 if position > self.utf16_len {
290 return Err(SourcePositionError::Utf16PositionOutOfBounds {
291 position,
292 len: self.utf16_len,
293 });
294 }
295
296 let checkpoint_index = self
297 .checkpoints
298 .partition_point(|checkpoint| checkpoint.utf16 <= position)
299 .saturating_sub(1);
300 let checkpoint = &self.checkpoints[checkpoint_index];
301 let byte_offset = checkpoint.byte + (position.get() - checkpoint.utf16.get());
302
303 if !self.text.is_char_boundary(byte_offset) {
304 return Err(SourcePositionError::Utf16PositionInsideSurrogatePair { position });
305 }
306
307 Ok(byte_offset)
308 }
309
310 pub fn range(&self, start: Utf16Pos, end: Utf16Pos) -> Result<TextRange, SourcePositionError> {
312 self.utf16_to_byte(start)?;
313 self.utf16_to_byte(end)?;
314 TextRange::new(start, end)
315 }
316
317 pub fn line_column(&self, position: Utf16Pos) -> Result<(usize, usize), SourcePositionError> {
323 self.utf16_to_byte(position)?;
324
325 let line_index = self
326 .line_starts
327 .partition_point(|line_start| *line_start <= position)
328 .saturating_sub(1);
329 let line_start = self.line_starts[line_index];
330 Ok((line_index, position.get() - line_start.get()))
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::{SourcePositionError, SourceText, TextRange, Utf16Pos};
337
338 #[test]
339 fn ascii_boundaries_round_trip() {
340 let source = SourceText::new("hello");
341
342 assert_eq!(source.len_utf16(), Utf16Pos::new(5));
343 for offset in 0..=5 {
344 assert_eq!(source.byte_to_utf16(offset), Ok(Utf16Pos::new(offset)));
345 assert_eq!(source.utf16_to_byte(Utf16Pos::new(offset)), Ok(offset));
346 }
347 assert_eq!(source.line_column(Utf16Pos::new(5)), Ok((0, 5)));
348 }
349
350 #[test]
351 fn bmp_code_points_preserve_utf16_width_but_not_byte_width() {
352 let source = SourceText::new("aΓ©δΈ");
353
354 assert_eq!(source.byte_to_utf16(0), Ok(Utf16Pos::new(0)));
355 assert_eq!(source.byte_to_utf16(1), Ok(Utf16Pos::new(1)));
356 assert_eq!(source.byte_to_utf16(3), Ok(Utf16Pos::new(2)));
357 assert_eq!(source.byte_to_utf16(6), Ok(Utf16Pos::new(3)));
358 assert_eq!(source.utf16_to_byte(Utf16Pos::new(2)), Ok(3));
359 assert_eq!(
360 source.byte_to_utf16(2),
361 Err(SourcePositionError::ByteOffsetInsideCodePoint { offset: 2 })
362 );
363 }
364
365 #[test]
366 fn astral_code_points_use_two_utf16_units() {
367 let source = SourceText::new("aπb");
368
369 assert_eq!(source.byte_to_utf16(1), Ok(Utf16Pos::new(1)));
370 assert_eq!(source.byte_to_utf16(5), Ok(Utf16Pos::new(3)));
371 assert_eq!(source.byte_to_utf16(6), Ok(Utf16Pos::new(4)));
372 assert_eq!(source.utf16_to_byte(Utf16Pos::new(1)), Ok(1));
373 assert_eq!(source.utf16_to_byte(Utf16Pos::new(3)), Ok(5));
374 assert_eq!(source.utf16_to_byte(Utf16Pos::new(4)), Ok(6));
375 assert_eq!(
376 source.utf16_to_byte(Utf16Pos::new(2)),
377 Err(SourcePositionError::Utf16PositionInsideSurrogatePair {
378 position: Utf16Pos::new(2),
379 })
380 );
381 }
382
383 #[test]
384 fn combining_marks_each_advance_the_utf16_column() {
385 let source = SourceText::new("e\u{301}x");
386
387 assert_eq!(source.byte_to_utf16(1), Ok(Utf16Pos::new(1)));
388 assert_eq!(source.byte_to_utf16(3), Ok(Utf16Pos::new(2)));
389 assert_eq!(source.byte_to_utf16(4), Ok(Utf16Pos::new(3)));
390 assert_eq!(source.line_column(Utf16Pos::new(2)), Ok((0, 2)));
391 }
392
393 #[test]
394 fn crlf_is_one_line_break_and_columns_are_utf16_units() {
395 let source = SourceText::new("a\r\nπ\nb");
396
397 assert_eq!(source.len_utf16(), Utf16Pos::new(7));
398 assert_eq!(source.line_column(Utf16Pos::new(0)), Ok((0, 0)));
399 assert_eq!(source.line_column(Utf16Pos::new(2)), Ok((0, 2)));
400 assert_eq!(source.line_column(Utf16Pos::new(3)), Ok((1, 0)));
401 assert_eq!(source.line_column(Utf16Pos::new(5)), Ok((1, 2)));
402 assert_eq!(source.line_column(Utf16Pos::new(6)), Ok((2, 0)));
403 assert_eq!(source.line_column(Utf16Pos::new(7)), Ok((2, 1)));
404 }
405
406 #[test]
407 fn unicode_line_and_paragraph_separators_advance_line_and_column() {
408 let source = SourceText::new("a\u{2028}b\u{2029}c");
416
417 assert_eq!(source.len_utf16(), Utf16Pos::new(5));
418
419 assert_eq!(source.line_column(Utf16Pos::new(0)), Ok((0, 0)));
421 assert_eq!(source.line_column(Utf16Pos::new(1)), Ok((0, 1)));
422
423 assert_eq!(source.line_column(Utf16Pos::new(2)), Ok((1, 0)));
425
426 assert_eq!(source.line_column(Utf16Pos::new(3)), Ok((1, 1)));
428
429 assert_eq!(source.line_column(Utf16Pos::new(4)), Ok((2, 0)));
431 assert_eq!(source.line_column(Utf16Pos::new(5)), Ok((2, 1)));
432 }
433
434 #[test]
435 fn empty_and_end_positions_are_valid_boundaries() {
436 let empty = SourceText::new("");
437 assert!(empty.is_empty());
438 assert_eq!(empty.byte_to_utf16(0), Ok(Utf16Pos::ZERO));
439 assert_eq!(empty.utf16_to_byte(Utf16Pos::ZERO), Ok(0));
440 assert_eq!(empty.line_column(Utf16Pos::ZERO), Ok((0, 0)));
441
442 let source = SourceText::new("π");
443 assert_eq!(source.byte_to_utf16(4), Ok(Utf16Pos::new(2)));
444 assert_eq!(source.utf16_to_byte(Utf16Pos::new(2)), Ok(4));
445 }
446
447 #[test]
448 fn invalid_byte_and_utf16_offsets_have_distinct_errors() {
449 let source = SourceText::new("π");
450
451 assert_eq!(
452 source.byte_to_utf16(1),
453 Err(SourcePositionError::ByteOffsetInsideCodePoint { offset: 1 })
454 );
455 assert_eq!(
456 source.byte_to_utf16(5),
457 Err(SourcePositionError::ByteOffsetOutOfBounds { offset: 5, len: 4 })
458 );
459 assert_eq!(
460 source.utf16_to_byte(Utf16Pos::new(1)),
461 Err(SourcePositionError::Utf16PositionInsideSurrogatePair {
462 position: Utf16Pos::new(1),
463 })
464 );
465 assert_eq!(
466 source.utf16_to_byte(Utf16Pos::new(3)),
467 Err(SourcePositionError::Utf16PositionOutOfBounds {
468 position: Utf16Pos::new(3),
469 len: Utf16Pos::new(2),
470 })
471 );
472 }
473
474 #[test]
475 fn ranges_cannot_be_reversed_and_source_ranges_validate_boundaries() {
476 assert_eq!(
477 TextRange::new(Utf16Pos::new(3), Utf16Pos::new(1)),
478 Err(SourcePositionError::RangeStartAfterEnd {
479 start: Utf16Pos::new(3),
480 end: Utf16Pos::new(1),
481 })
482 );
483
484 let source = SourceText::new("aπb");
485 let range = source
486 .range(Utf16Pos::new(1), Utf16Pos::new(3))
487 .expect("the emoji's outer boundaries are valid");
488 assert_eq!(range.start(), Utf16Pos::new(1));
489 assert_eq!(range.end(), Utf16Pos::new(3));
490 assert_eq!(range.len(), 2);
491 assert_eq!(
492 source.range(Utf16Pos::new(3), Utf16Pos::new(1)),
493 Err(SourcePositionError::RangeStartAfterEnd {
494 start: Utf16Pos::new(3),
495 end: Utf16Pos::new(1),
496 })
497 );
498 assert_eq!(
499 source.range(Utf16Pos::new(1), Utf16Pos::new(2)),
500 Err(SourcePositionError::Utf16PositionInsideSurrogatePair {
501 position: Utf16Pos::new(2),
502 })
503 );
504 }
505}