ruff_source_file/line_index.rs
1use std::fmt;
2use std::fmt::{Debug, Formatter};
3use std::num::{NonZeroUsize, ParseIntError};
4use std::ops::Deref;
5use std::str::FromStr;
6use std::sync::Arc;
7
8use crate::{LineColumn, SourceLocation};
9use ruff_text_size::{TextLen, TextRange, TextSize};
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13/// Index for fast [byte offset](TextSize) to [`LineColumn`] conversions.
14///
15/// Cloning a [`LineIndex`] is cheap because it only requires bumping a reference count.
16#[derive(Clone, Eq, PartialEq)]
17#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
18pub struct LineIndex {
19 inner: Arc<LineIndexInner>,
20}
21
22#[derive(Eq, PartialEq)]
23#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
24struct LineIndexInner {
25 line_starts: Vec<TextSize>,
26 kind: IndexKind,
27}
28
29impl LineIndex {
30 /// Builds the [`LineIndex`] from the source text of a file.
31 pub fn from_source_text(text: &str) -> Self {
32 let mut line_starts: Vec<TextSize> = Vec::with_capacity(text.len() / 88);
33 line_starts.push(TextSize::default());
34
35 let bytes = text.as_bytes();
36
37 assert!(u32::try_from(bytes.len()).is_ok());
38
39 for i in memchr::memchr2_iter(b'\n', b'\r', bytes) {
40 // Skip `\r` in `\r\n` sequences (only count the `\n`).
41 if bytes[i] == b'\r' && bytes.get(i + 1) == Some(&b'\n') {
42 continue;
43 }
44 // SAFETY: Assertion above guarantees `i <= u32::MAX`
45 #[expect(clippy::cast_possible_truncation)]
46 line_starts.push(TextSize::from(i as u32) + TextSize::from(1));
47 }
48
49 // Determine whether the source text is ASCII.
50 //
51 // Empirically, this simple loop is auto-vectorized by LLVM and benchmarks faster than both
52 // `str::is_ascii()` and hand-written SIMD.
53 let mut has_non_ascii = false;
54 for byte in bytes {
55 has_non_ascii |= !byte.is_ascii();
56 }
57
58 let kind = if has_non_ascii {
59 IndexKind::Utf8
60 } else {
61 IndexKind::Ascii
62 };
63
64 Self {
65 inner: Arc::new(LineIndexInner { line_starts, kind }),
66 }
67 }
68
69 fn kind(&self) -> IndexKind {
70 self.inner.kind
71 }
72
73 /// Returns the line and column number for an UTF-8 byte offset.
74 ///
75 /// The `column` number is the nth-character of the line, except for the first line
76 /// where it doesn't include the UTF-8 BOM marker at the start of the file.
77 ///
78 /// ### BOM handling
79 ///
80 /// For files starting with a UTF-8 BOM marker, the byte offsets
81 /// in the range `0...3` are all mapped to line 0 and column 0.
82 /// Because of this, the conversion isn't lossless.
83 ///
84 /// ## Examples
85 ///
86 /// ```
87 /// # use ruff_text_size::TextSize;
88 /// # use ruff_source_file::{LineIndex, OneIndexed, LineColumn};
89 /// let source = format!("\u{FEFF}{}", "def a():\n pass");
90 /// let index = LineIndex::from_source_text(&source);
91 ///
92 /// // Before BOM, maps to after BOM
93 /// assert_eq!(
94 /// index.line_column(TextSize::from(0), &source),
95 /// LineColumn { line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) }
96 /// );
97 ///
98 /// // After BOM, maps to after BOM
99 /// assert_eq!(
100 /// index.line_column(TextSize::from(3), &source),
101 /// LineColumn { line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(0) }
102 /// );
103 ///
104 /// assert_eq!(
105 /// index.line_column(TextSize::from(7), &source),
106 /// LineColumn { line: OneIndexed::from_zero_indexed(0), column: OneIndexed::from_zero_indexed(4) }
107 /// );
108 /// assert_eq!(
109 /// index.line_column(TextSize::from(16), &source),
110 /// LineColumn { line: OneIndexed::from_zero_indexed(1), column: OneIndexed::from_zero_indexed(4) }
111 /// );
112 /// ```
113 ///
114 /// ## Panics
115 ///
116 /// If the byte offset isn't within the bounds of `content`.
117 pub fn line_column(&self, offset: TextSize, content: &str) -> LineColumn {
118 let location = self.source_location(offset, content, PositionEncoding::Utf32);
119
120 // Don't count the BOM character as a column, but only on the first line.
121 let column = if location.line.to_zero_indexed() == 0 && content.starts_with('\u{feff}') {
122 location.character_offset.saturating_sub(1)
123 } else {
124 location.character_offset
125 };
126
127 LineColumn {
128 line: location.line,
129 column,
130 }
131 }
132
133 /// Given a UTF-8 byte offset, returns the line and character offset according to the given encoding.
134 ///
135 /// ### BOM handling
136 ///
137 /// Unlike [`Self::line_column`], this method does not skip the BOM character at the start of the file.
138 /// This allows for bidirectional mapping between [`SourceLocation`] and [`TextSize`] (see [`Self::offset`]).
139 ///
140 /// ## Examples
141 ///
142 /// ```
143 /// # use ruff_text_size::TextSize;
144 /// # use ruff_source_file::{LineIndex, OneIndexed, LineColumn, SourceLocation, PositionEncoding, Line};
145 /// let source = format!("\u{FEFF}{}", "def a():\n pass");
146 /// let index = LineIndex::from_source_text(&source);
147 ///
148 /// // Before BOM, maps to character 0
149 /// assert_eq!(
150 /// index.source_location(TextSize::from(0), &source, PositionEncoding::Utf32),
151 /// SourceLocation { line: OneIndexed::from_zero_indexed(0), character_offset: OneIndexed::from_zero_indexed(0) }
152 /// );
153 ///
154 /// // After BOM, maps to after BOM
155 /// assert_eq!(
156 /// index.source_location(TextSize::from(3), &source, PositionEncoding::Utf32),
157 /// SourceLocation { line: OneIndexed::from_zero_indexed(0), character_offset: OneIndexed::from_zero_indexed(1) }
158 /// );
159 ///
160 /// assert_eq!(
161 /// index.source_location(TextSize::from(7), &source, PositionEncoding::Utf32),
162 /// SourceLocation { line: OneIndexed::from_zero_indexed(0), character_offset: OneIndexed::from_zero_indexed(5) }
163 /// );
164 /// assert_eq!(
165 /// index.source_location(TextSize::from(16), &source, PositionEncoding::Utf32),
166 /// SourceLocation { line: OneIndexed::from_zero_indexed(1), character_offset: OneIndexed::from_zero_indexed(4) }
167 /// );
168 /// ```
169 ///
170 /// ## Panics
171 ///
172 /// If the UTF-8 byte offset is out of bounds of `text`.
173 pub fn source_location(
174 &self,
175 offset: TextSize,
176 text: &str,
177 encoding: PositionEncoding,
178 ) -> SourceLocation {
179 let line = self.line_index(offset);
180 let line_start = self.line_start(line, text);
181
182 let character_offset =
183 self.characters_between(TextRange::new(line_start, offset), text, encoding);
184
185 SourceLocation {
186 line,
187 character_offset: OneIndexed::from_zero_indexed(character_offset),
188 }
189 }
190
191 fn characters_between(
192 &self,
193 range: TextRange,
194 text: &str,
195 encoding: PositionEncoding,
196 ) -> usize {
197 if self.is_ascii() {
198 return (range.end() - range.start()).to_usize();
199 }
200
201 match encoding {
202 PositionEncoding::Utf8 => (range.end() - range.start()).to_usize(),
203 PositionEncoding::Utf16 => {
204 let up_to_character = &text[range];
205 up_to_character.encode_utf16().count()
206 }
207 PositionEncoding::Utf32 => {
208 let up_to_character = &text[range];
209 up_to_character.chars().count()
210 }
211 }
212 }
213
214 /// Returns the length of the line in characters, respecting the given encoding
215 pub fn line_len(&self, line: OneIndexed, text: &str, encoding: PositionEncoding) -> usize {
216 let line_range = self.line_range(line, text);
217
218 self.characters_between(line_range, text, encoding)
219 }
220
221 /// Return the number of lines in the source code.
222 pub fn line_count(&self) -> usize {
223 self.line_starts().len()
224 }
225
226 /// Returns `true` if the text only consists of ASCII characters
227 fn is_ascii(&self) -> bool {
228 self.kind().is_ascii()
229 }
230
231 /// Returns the row number for a given offset.
232 ///
233 /// ## Examples
234 ///
235 /// ```
236 /// # use ruff_text_size::TextSize;
237 /// # use ruff_source_file::{LineIndex, OneIndexed, LineColumn};
238 /// let source = "def a():\n pass";
239 /// let index = LineIndex::from_source_text(source);
240 ///
241 /// assert_eq!(index.line_index(TextSize::from(0)), OneIndexed::from_zero_indexed(0));
242 /// assert_eq!(index.line_index(TextSize::from(4)), OneIndexed::from_zero_indexed(0));
243 /// assert_eq!(index.line_index(TextSize::from(13)), OneIndexed::from_zero_indexed(1));
244 /// ```
245 ///
246 /// ## Panics
247 ///
248 /// If the offset is out of bounds.
249 pub fn line_index(&self, offset: TextSize) -> OneIndexed {
250 match self.line_starts().binary_search(&offset) {
251 // Offset is at the start of a line
252 Ok(row) => OneIndexed::from_zero_indexed(row),
253 Err(row) => {
254 // SAFETY: Safe because the index always contains an entry for the offset 0
255 OneIndexed::from_zero_indexed(row - 1)
256 }
257 }
258 }
259
260 /// Returns the [byte offset](TextSize) for the `line` with the given index.
261 pub fn line_start(&self, line: OneIndexed, contents: &str) -> TextSize {
262 let row_index = line.to_zero_indexed();
263 let starts = self.line_starts();
264
265 // If start-of-line position after last line
266 if row_index == starts.len() {
267 contents.text_len()
268 } else {
269 starts[row_index]
270 }
271 }
272
273 /// Returns the [byte offset](TextSize) of the `line`'s end.
274 /// The offset is the end of the line, up to and including the newline character ending the line (if any).
275 pub fn line_end(&self, line: OneIndexed, contents: &str) -> TextSize {
276 let row_index = line.to_zero_indexed();
277 let starts = self.line_starts();
278
279 // If start-of-line position after last line
280 if row_index.saturating_add(1) >= starts.len() {
281 contents.text_len()
282 } else {
283 starts[row_index + 1]
284 }
285 }
286
287 /// Returns the [byte offset](TextSize) of the `line`'s end.
288 /// The offset is the end of the line, excluding the newline character ending the line (if any).
289 pub(crate) fn line_end_exclusive(&self, line: OneIndexed, contents: &str) -> TextSize {
290 let row_index = line.to_zero_indexed();
291 let starts = self.line_starts();
292
293 // If start-of-line position after last line
294 if row_index.saturating_add(1) >= starts.len() {
295 contents.text_len()
296 } else {
297 let next_line_start = starts[row_index + 1].to_usize();
298 let bytes = contents.as_bytes();
299
300 let line_ending_len = if bytes[..next_line_start].ends_with(b"\r\n") {
301 2
302 } else {
303 1
304 };
305 starts[row_index + 1] - TextSize::new(line_ending_len)
306 }
307 }
308
309 /// Returns the [`TextRange`] of the `line` with the given index.
310 /// The start points to the first character's [byte offset](TextSize), the end up to, and including
311 /// the newline character ending the line (if any).
312 pub fn line_range(&self, line: OneIndexed, contents: &str) -> TextRange {
313 let starts = self.line_starts();
314
315 if starts.len() == line.to_zero_indexed() {
316 TextRange::empty(contents.text_len())
317 } else {
318 TextRange::new(
319 self.line_start(line, contents),
320 self.line_start(line.saturating_add(1), contents),
321 )
322 }
323 }
324
325 /// Returns the [UTF-8 byte offset](TextSize) at `line` and `character` where character is counted using the given encoding.
326 ///
327 /// ## Examples
328 ///
329 /// ### ASCII only source text
330 ///
331 /// ```
332 /// # use ruff_source_file::{SourceLocation, LineIndex, OneIndexed, PositionEncoding};
333 /// # use ruff_text_size::TextSize;
334 /// let source = r#"a = 4
335 /// c = "some string"
336 /// x = b"#;
337 ///
338 /// let index = LineIndex::from_source_text(source);
339 ///
340 /// // First line, first character
341 /// assert_eq!(
342 /// index.offset(
343 /// SourceLocation {
344 /// line: OneIndexed::from_zero_indexed(0),
345 /// character_offset: OneIndexed::from_zero_indexed(0)
346 /// },
347 /// source,
348 /// PositionEncoding::Utf32,
349 /// ),
350 /// TextSize::new(0)
351 /// );
352 ///
353 /// assert_eq!(
354 /// index.offset(
355 /// SourceLocation {
356 /// line: OneIndexed::from_zero_indexed(1),
357 /// character_offset: OneIndexed::from_zero_indexed(4)
358 /// },
359 /// source,
360 /// PositionEncoding::Utf32,
361 /// ),
362 /// TextSize::new(10)
363 /// );
364 ///
365 /// // Offset past the end of the first line
366 /// assert_eq!(
367 /// index.offset(
368 /// SourceLocation {
369 /// line: OneIndexed::from_zero_indexed(0),
370 /// character_offset: OneIndexed::from_zero_indexed(10)
371 /// },
372 /// source,
373 /// PositionEncoding::Utf32,
374 /// ),
375 /// TextSize::new(6)
376 /// );
377 ///
378 /// // Offset past the end of the file
379 /// assert_eq!(
380 /// index.offset(
381 /// SourceLocation {
382 /// line: OneIndexed::from_zero_indexed(3),
383 /// character_offset: OneIndexed::from_zero_indexed(0)
384 /// },
385 /// source,
386 /// PositionEncoding::Utf32,
387 /// ),
388 /// TextSize::new(29)
389 /// );
390 /// ```
391 ///
392 /// ### Non-ASCII source text
393 ///
394 /// ```
395 /// use ruff_source_file::{LineIndex, OneIndexed, SourceLocation, PositionEncoding};
396 /// use ruff_text_size::TextSize;
397 /// let source = format!("\u{FEFF}{}", r#"a = 4
398 /// c = "❤️"
399 /// x = b"#);
400 ///
401 /// let index = LineIndex::from_source_text(&source);
402 ///
403 /// // First line, first character, points at the BOM
404 /// assert_eq!(
405 /// index.offset(
406 /// SourceLocation {
407 /// line: OneIndexed::from_zero_indexed(0),
408 /// character_offset: OneIndexed::from_zero_indexed(0)
409 /// },
410 /// &source,
411 /// PositionEncoding::Utf32,
412 /// ),
413 /// TextSize::new(0)
414 /// );
415 ///
416 /// // First line, after the BOM
417 /// assert_eq!(
418 /// index.offset(
419 /// SourceLocation {
420 /// line: OneIndexed::from_zero_indexed(0),
421 /// character_offset: OneIndexed::from_zero_indexed(1)
422 /// },
423 /// &source,
424 /// PositionEncoding::Utf32,
425 /// ),
426 /// TextSize::new(3)
427 /// );
428 ///
429 /// // second line, 7th character, after emoji, UTF32
430 /// assert_eq!(
431 /// index.offset(
432 /// SourceLocation {
433 /// line: OneIndexed::from_zero_indexed(1),
434 /// character_offset: OneIndexed::from_zero_indexed(7)
435 /// },
436 /// &source,
437 /// PositionEncoding::Utf32,
438 /// ),
439 /// TextSize::new(20)
440 /// );
441 ///
442 /// // Second line, 7th character, after emoji, UTF 16
443 /// assert_eq!(
444 /// index.offset(
445 /// SourceLocation {
446 /// line: OneIndexed::from_zero_indexed(1),
447 /// character_offset: OneIndexed::from_zero_indexed(7)
448 /// },
449 /// &source,
450 /// PositionEncoding::Utf16,
451 /// ),
452 /// TextSize::new(20)
453 /// );
454 ///
455 ///
456 /// // Offset past the end of the second line
457 /// assert_eq!(
458 /// index.offset(
459 /// SourceLocation {
460 /// line: OneIndexed::from_zero_indexed(1),
461 /// character_offset: OneIndexed::from_zero_indexed(10)
462 /// },
463 /// &source,
464 /// PositionEncoding::Utf32,
465 /// ),
466 /// TextSize::new(22)
467 /// );
468 ///
469 /// // Offset past the end of the file
470 /// assert_eq!(
471 /// index.offset(
472 /// SourceLocation {
473 /// line: OneIndexed::from_zero_indexed(3),
474 /// character_offset: OneIndexed::from_zero_indexed(0)
475 /// },
476 /// &source,
477 /// PositionEncoding::Utf32,
478 /// ),
479 /// TextSize::new(27)
480 /// );
481 /// ```
482 pub fn offset(
483 &self,
484 position: SourceLocation,
485 text: &str,
486 position_encoding: PositionEncoding,
487 ) -> TextSize {
488 // If start-of-line position after last line
489 if position.line.to_zero_indexed() > self.line_starts().len() {
490 return text.text_len();
491 }
492
493 let line_range = self.line_range(position.line, text);
494
495 let character_offset = position.character_offset.to_zero_indexed();
496 let character_byte_offset = if self.is_ascii() {
497 TextSize::try_from(character_offset).unwrap()
498 } else {
499 let line = &text[line_range];
500
501 match position_encoding {
502 PositionEncoding::Utf8 => {
503 TextSize::try_from(position.character_offset.to_zero_indexed()).unwrap()
504 }
505 PositionEncoding::Utf16 => {
506 let mut byte_offset = TextSize::new(0);
507 let mut utf16_code_unit_offset = 0;
508
509 for c in line.chars() {
510 if utf16_code_unit_offset >= character_offset {
511 break;
512 }
513
514 // Count characters encoded as two 16 bit words as 2 characters.
515 byte_offset += c.text_len();
516 utf16_code_unit_offset += c.len_utf16();
517 }
518
519 byte_offset
520 }
521 PositionEncoding::Utf32 => line
522 .chars()
523 .take(position.character_offset.to_zero_indexed())
524 .map(ruff_text_size::TextLen::text_len)
525 .sum(),
526 }
527 };
528
529 line_range.start() + character_byte_offset.clamp(TextSize::new(0), line_range.len())
530 }
531
532 /// Returns the [byte offsets](TextSize) for every line
533 pub fn line_starts(&self) -> &[TextSize] {
534 &self.inner.line_starts
535 }
536}
537
538impl Deref for LineIndex {
539 type Target = [TextSize];
540
541 fn deref(&self) -> &Self::Target {
542 self.line_starts()
543 }
544}
545
546impl Debug for LineIndex {
547 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
548 f.debug_list().entries(self.line_starts()).finish()
549 }
550}
551
552#[derive(Debug, Clone, Copy, Eq, PartialEq)]
553#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
554enum IndexKind {
555 /// Optimized index for an ASCII only document
556 Ascii,
557
558 /// Index for UTF8 documents
559 Utf8,
560}
561
562impl IndexKind {
563 const fn is_ascii(self) -> bool {
564 matches!(self, IndexKind::Ascii)
565 }
566}
567
568/// Type-safe wrapper for a value whose logical range starts at `1`, for
569/// instance the line or column numbers in a file
570///
571/// Internally this is represented as a [`NonZeroUsize`], this enables some
572/// memory optimizations
573#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
574#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
575pub struct OneIndexed(NonZeroUsize);
576
577impl OneIndexed {
578 /// The largest value that can be represented by this integer type
579 pub const MAX: Self = Self::new(usize::MAX).unwrap();
580 // SAFETY: These constants are being initialized with non-zero values
581 /// The smallest value that can be represented by this integer type.
582 pub const MIN: Self = Self::new(1).unwrap();
583 const ONE: NonZeroUsize = NonZeroUsize::new(1).unwrap();
584
585 /// Creates a non-zero if the given value is not zero.
586 pub const fn new(value: usize) -> Option<Self> {
587 match NonZeroUsize::new(value) {
588 Some(value) => Some(Self(value)),
589 None => None,
590 }
591 }
592
593 /// Construct a new [`OneIndexed`] from a zero-indexed value
594 pub const fn from_zero_indexed(value: usize) -> Self {
595 Self(Self::ONE.saturating_add(value))
596 }
597
598 /// Returns the value as a primitive type.
599 pub const fn get(self) -> usize {
600 self.0.get()
601 }
602
603 /// Return the zero-indexed primitive value for this [`OneIndexed`]
604 pub const fn to_zero_indexed(self) -> usize {
605 self.0.get() - 1
606 }
607
608 /// Saturating integer addition. Computes `self + rhs`, saturating at
609 /// the numeric bounds instead of overflowing.
610 #[must_use]
611 pub const fn saturating_add(self, rhs: usize) -> Self {
612 match NonZeroUsize::new(self.0.get().saturating_add(rhs)) {
613 Some(value) => Self(value),
614 None => Self::MAX,
615 }
616 }
617
618 /// Saturating integer subtraction. Computes `self - rhs`, saturating
619 /// at the numeric bounds instead of overflowing.
620 #[must_use]
621 pub const fn saturating_sub(self, rhs: usize) -> Self {
622 match NonZeroUsize::new(self.0.get().saturating_sub(rhs)) {
623 Some(value) => Self(value),
624 None => Self::MIN,
625 }
626 }
627
628 /// Checked addition. Returns `None` if overflow occurred.
629 #[must_use]
630 pub fn checked_add(self, rhs: Self) -> Option<Self> {
631 self.0.checked_add(rhs.0.get()).map(Self)
632 }
633
634 /// Checked subtraction. Returns `None` if overflow occurred.
635 #[must_use]
636 pub fn checked_sub(self, rhs: Self) -> Option<Self> {
637 self.0.get().checked_sub(rhs.get()).and_then(Self::new)
638 }
639
640 /// Calculate the number of digits in `self`.
641 ///
642 /// This is primarily intended for computing the length of the string representation for
643 /// formatted printing.
644 ///
645 /// # Examples
646 ///
647 /// ```
648 /// use ruff_source_file::OneIndexed;
649 ///
650 /// let one = OneIndexed::new(1).unwrap();
651 /// assert_eq!(one.digits().get(), 1);
652 ///
653 /// let hundred = OneIndexed::new(100).unwrap();
654 /// assert_eq!(hundred.digits().get(), 3);
655 ///
656 /// let thousand = OneIndexed::new(1000).unwrap();
657 /// assert_eq!(thousand.digits().get(), 4);
658 /// ```
659 pub const fn digits(self) -> NonZeroUsize {
660 // Safety: the 1+ ensures this is always non-zero, and
661 // `usize::MAX.ilog10()` << `usize::MAX`, so the result is always safe
662 // to cast to a usize, even though it's returned as a u32
663 // (u64::MAX.ilog10() is 19).
664 NonZeroUsize::new(1 + self.0.get().ilog10() as usize).unwrap()
665 }
666}
667
668impl Default for OneIndexed {
669 fn default() -> Self {
670 Self::MIN
671 }
672}
673
674impl fmt::Display for OneIndexed {
675 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
676 std::fmt::Debug::fmt(&self.0.get(), f)
677 }
678}
679
680impl FromStr for OneIndexed {
681 type Err = ParseIntError;
682 fn from_str(s: &str) -> Result<Self, Self::Err> {
683 Ok(OneIndexed(NonZeroUsize::from_str(s)?))
684 }
685}
686
687#[derive(Copy, Clone, Debug)]
688pub enum PositionEncoding {
689 /// Character offsets count the number of bytes from the start of the line.
690 Utf8,
691
692 /// Character offsets count the number of UTF-16 code units from the start of the line.
693 Utf16,
694
695 /// Character offsets count the number of UTF-32 code points units (the same as number of characters in Rust)
696 /// from the start of the line.
697 Utf32,
698}
699
700#[cfg(test)]
701mod tests {
702 use ruff_text_size::TextSize;
703
704 use crate::line_index::LineIndex;
705 use crate::{LineColumn, OneIndexed};
706
707 #[test]
708 fn ascii_index() {
709 let index = LineIndex::from_source_text("");
710 assert_eq!(index.line_starts(), &[TextSize::from(0)]);
711
712 let index = LineIndex::from_source_text("x = 1");
713 assert_eq!(index.line_starts(), &[TextSize::from(0)]);
714
715 let index = LineIndex::from_source_text("x = 1\n");
716 assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(6)]);
717
718 let index = LineIndex::from_source_text("x = 1\ny = 2\nz = x + y\n");
719 assert_eq!(
720 index.line_starts(),
721 &[
722 TextSize::from(0),
723 TextSize::from(6),
724 TextSize::from(12),
725 TextSize::from(22)
726 ]
727 );
728 }
729
730 #[test]
731 fn ascii_source_location() {
732 let contents = "x = 1\ny = 2";
733 let index = LineIndex::from_source_text(contents);
734
735 // First row.
736 let loc = index.line_column(TextSize::from(2), contents);
737 assert_eq!(
738 loc,
739 LineColumn {
740 line: OneIndexed::from_zero_indexed(0),
741 column: OneIndexed::from_zero_indexed(2)
742 }
743 );
744
745 // Second row.
746 let loc = index.line_column(TextSize::from(6), contents);
747 assert_eq!(
748 loc,
749 LineColumn {
750 line: OneIndexed::from_zero_indexed(1),
751 column: OneIndexed::from_zero_indexed(0)
752 }
753 );
754
755 let loc = index.line_column(TextSize::from(11), contents);
756 assert_eq!(
757 loc,
758 LineColumn {
759 line: OneIndexed::from_zero_indexed(1),
760 column: OneIndexed::from_zero_indexed(5)
761 }
762 );
763 }
764
765 #[test]
766 fn ascii_carriage_return() {
767 let contents = "x = 4\ry = 3";
768 let index = LineIndex::from_source_text(contents);
769 assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(6)]);
770
771 assert_eq!(
772 index.line_column(TextSize::from(4), contents),
773 LineColumn {
774 line: OneIndexed::from_zero_indexed(0),
775 column: OneIndexed::from_zero_indexed(4)
776 }
777 );
778 assert_eq!(
779 index.line_column(TextSize::from(6), contents),
780 LineColumn {
781 line: OneIndexed::from_zero_indexed(1),
782 column: OneIndexed::from_zero_indexed(0)
783 }
784 );
785 assert_eq!(
786 index.line_column(TextSize::from(7), contents),
787 LineColumn {
788 line: OneIndexed::from_zero_indexed(1),
789 column: OneIndexed::from_zero_indexed(1)
790 }
791 );
792 }
793
794 #[test]
795 fn ascii_carriage_return_newline() {
796 let contents = "x = 4\r\ny = 3";
797 let index = LineIndex::from_source_text(contents);
798 assert_eq!(index.line_starts(), &[TextSize::from(0), TextSize::from(7)]);
799
800 assert_eq!(
801 index.line_column(TextSize::from(4), contents),
802 LineColumn {
803 line: OneIndexed::from_zero_indexed(0),
804 column: OneIndexed::from_zero_indexed(4)
805 }
806 );
807 assert_eq!(
808 index.line_column(TextSize::from(7), contents),
809 LineColumn {
810 line: OneIndexed::from_zero_indexed(1),
811 column: OneIndexed::from_zero_indexed(0)
812 }
813 );
814 assert_eq!(
815 index.line_column(TextSize::from(8), contents),
816 LineColumn {
817 line: OneIndexed::from_zero_indexed(1),
818 column: OneIndexed::from_zero_indexed(1)
819 }
820 );
821 }
822
823 #[test]
824 fn line_end_exclusive_handles_different_line_endings() {
825 let lf_contents = "a\nb";
826 let lf_index = LineIndex::from_source_text(lf_contents);
827 assert_eq!(
828 lf_index.line_end_exclusive(OneIndexed::from_zero_indexed(0), lf_contents),
829 TextSize::from(1)
830 );
831 assert_eq!(
832 lf_index.line_end_exclusive(OneIndexed::from_zero_indexed(1), lf_contents),
833 TextSize::from(3)
834 );
835
836 let crlf_contents = "a\r\nb";
837 let crlf_index = LineIndex::from_source_text(crlf_contents);
838 assert_eq!(
839 crlf_index.line_end_exclusive(OneIndexed::from_zero_indexed(0), crlf_contents),
840 TextSize::from(1)
841 );
842 assert_eq!(
843 crlf_index.line_end_exclusive(OneIndexed::from_zero_indexed(1), crlf_contents),
844 TextSize::from(4)
845 );
846
847 let cr_contents = "a\rb";
848 let cr_index = LineIndex::from_source_text(cr_contents);
849 assert_eq!(
850 cr_index.line_end_exclusive(OneIndexed::from_zero_indexed(0), cr_contents),
851 TextSize::from(1)
852 );
853 assert_eq!(
854 cr_index.line_end_exclusive(OneIndexed::from_zero_indexed(1), cr_contents),
855 TextSize::from(3)
856 );
857 }
858
859 #[test]
860 fn utf8_index() {
861 let index = LineIndex::from_source_text("x = '🫣'");
862 assert_eq!(index.line_count(), 1);
863 assert_eq!(index.line_starts(), &[TextSize::from(0)]);
864
865 let index = LineIndex::from_source_text("x = '🫣'\n");
866 assert_eq!(index.line_count(), 2);
867 assert_eq!(
868 index.line_starts(),
869 &[TextSize::from(0), TextSize::from(11)]
870 );
871
872 let index = LineIndex::from_source_text("x = '🫣'\ny = 2\nz = x + y\n");
873 assert_eq!(index.line_count(), 4);
874 assert_eq!(
875 index.line_starts(),
876 &[
877 TextSize::from(0),
878 TextSize::from(11),
879 TextSize::from(17),
880 TextSize::from(27)
881 ]
882 );
883
884 let index = LineIndex::from_source_text("# 🫣\nclass Foo:\n \"\"\".\"\"\"");
885 assert_eq!(index.line_count(), 3);
886 assert_eq!(
887 index.line_starts(),
888 &[TextSize::from(0), TextSize::from(7), TextSize::from(18)]
889 );
890 }
891
892 #[test]
893 fn utf8_carriage_return() {
894 let contents = "x = '🫣'\ry = 3";
895 let index = LineIndex::from_source_text(contents);
896 assert_eq!(index.line_count(), 2);
897 assert_eq!(
898 index.line_starts(),
899 &[TextSize::from(0), TextSize::from(11)]
900 );
901
902 // Second '
903 assert_eq!(
904 index.line_column(TextSize::from(9), contents),
905 LineColumn {
906 line: OneIndexed::from_zero_indexed(0),
907 column: OneIndexed::from_zero_indexed(6)
908 }
909 );
910 assert_eq!(
911 index.line_column(TextSize::from(11), contents),
912 LineColumn {
913 line: OneIndexed::from_zero_indexed(1),
914 column: OneIndexed::from_zero_indexed(0)
915 }
916 );
917 assert_eq!(
918 index.line_column(TextSize::from(12), contents),
919 LineColumn {
920 line: OneIndexed::from_zero_indexed(1),
921 column: OneIndexed::from_zero_indexed(1)
922 }
923 );
924 }
925
926 #[test]
927 fn utf8_carriage_return_newline() {
928 let contents = "x = '🫣'\r\ny = 3";
929 let index = LineIndex::from_source_text(contents);
930 assert_eq!(index.line_count(), 2);
931 assert_eq!(
932 index.line_starts(),
933 &[TextSize::from(0), TextSize::from(12)]
934 );
935
936 // Second '
937 assert_eq!(
938 index.line_column(TextSize::from(9), contents),
939 LineColumn {
940 line: OneIndexed::from_zero_indexed(0),
941 column: OneIndexed::from_zero_indexed(6)
942 }
943 );
944 assert_eq!(
945 index.line_column(TextSize::from(12), contents),
946 LineColumn {
947 line: OneIndexed::from_zero_indexed(1),
948 column: OneIndexed::from_zero_indexed(0)
949 }
950 );
951 assert_eq!(
952 index.line_column(TextSize::from(13), contents),
953 LineColumn {
954 line: OneIndexed::from_zero_indexed(1),
955 column: OneIndexed::from_zero_indexed(1)
956 }
957 );
958 }
959
960 #[test]
961 fn utf8_byte_offset() {
962 let contents = "x = '☃'\ny = 2";
963 let index = LineIndex::from_source_text(contents);
964 assert_eq!(
965 index.line_starts(),
966 &[TextSize::from(0), TextSize::from(10)]
967 );
968
969 // First row.
970 let loc = index.line_column(TextSize::from(0), contents);
971 assert_eq!(
972 loc,
973 LineColumn {
974 line: OneIndexed::from_zero_indexed(0),
975 column: OneIndexed::from_zero_indexed(0)
976 }
977 );
978
979 let loc = index.line_column(TextSize::from(5), contents);
980 assert_eq!(
981 loc,
982 LineColumn {
983 line: OneIndexed::from_zero_indexed(0),
984 column: OneIndexed::from_zero_indexed(5)
985 }
986 );
987
988 let loc = index.line_column(TextSize::from(8), contents);
989 assert_eq!(
990 loc,
991 LineColumn {
992 line: OneIndexed::from_zero_indexed(0),
993 column: OneIndexed::from_zero_indexed(6)
994 }
995 );
996
997 // Second row.
998 let loc = index.line_column(TextSize::from(10), contents);
999 assert_eq!(
1000 loc,
1001 LineColumn {
1002 line: OneIndexed::from_zero_indexed(1),
1003 column: OneIndexed::from_zero_indexed(0)
1004 }
1005 );
1006
1007 // One-past-the-end.
1008 let loc = index.line_column(TextSize::from(15), contents);
1009 assert_eq!(
1010 loc,
1011 LineColumn {
1012 line: OneIndexed::from_zero_indexed(1),
1013 column: OneIndexed::from_zero_indexed(5)
1014 }
1015 );
1016 }
1017}