line-index-0.1.2/.cargo_vcs_info.json0000644000000001540000000000100130700ustar { "git": { "sha1": "3bda0cc2c07f6330b543be2c12c5e156847ba94e" }, "path_in_vcs": "lib/line-index" }line-index-0.1.2/Cargo.toml0000644000000037300000000000100110710ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "line-index" version = "0.1.2" build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Maps flat `TextSize` offsets to/from `(line, column)` representation." readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://github.com/rust-lang/rust-analyzer/tree/master/lib/line-index" [lib] name = "line_index" path = "src/lib.rs" [dependencies.nohash-hasher] version = "0.2.0" [dependencies.text-size] version = "1.1.1" [dev-dependencies.oorandom] version = "11.1.3" [lints.clippy] assigning_clones = "allow" dbg_macro = "warn" enum_variant_names = "allow" len_without_is_empty = "allow" new_ret_no_self = "allow" print_stderr = "warn" print_stdout = "warn" rc_buffer = "warn" result_unit_err = "allow" single_match = "allow" str_to_string = "warn" todo = "warn" too_long_first_doc_paragraph = "allow" too_many_arguments = "allow" type_complexity = "allow" useless_asref = "allow" wrong_self_convention = "allow" [lints.clippy.complexity] level = "warn" priority = -1 [lints.clippy.correctness] level = "deny" priority = -1 [lints.clippy.perf] level = "deny" priority = -1 [lints.clippy.restriction] level = "allow" priority = -1 [lints.clippy.style] level = "warn" priority = -1 [lints.clippy.suspicious] level = "warn" priority = -1 [lints.rust] elided_lifetimes_in_paths = "warn" explicit_outlives_requirements = "warn" unreachable_pub = "warn" unsafe_op_in_unsafe_fn = "warn" unused_extern_crates = "warn" unused_lifetimes = "warn" line-index-0.1.2/Cargo.toml.orig000064400000000000000000000006101046102023000145440ustar 00000000000000[package] name = "line-index" version = "0.1.2" description = "Maps flat `TextSize` offsets to/from `(line, column)` representation." license = "MIT OR Apache-2.0" repository = "https://github.com/rust-lang/rust-analyzer/tree/master/lib/line-index" edition = "2021" [dependencies] text-size = "1.1.1" nohash-hasher = "0.2.0" [dev-dependencies] oorandom = "11.1.3" [lints] workspace = true line-index-0.1.2/README.md000064400000000000000000000013651046102023000131440ustar 00000000000000# line-index This crate is developed as part of `rust-analyzer`. line-index is a library to convert between text offsets and corresponding line/column coordinates. ## Installation To add this crate to a project simply run `cargo add line-index`. ## Usage The main structure is `LineIndex`. It is constructed with an UTF-8 string, but also supports UTF-16 and UTF-32 offsets. ### Example ```rust use line_index::LineIndex; let line_index = LineIndex::new("This is a\nmulti-line\ntext."); line_index.line_col(3.into()); // LineCol { line: 0, col: 3 } line_index.line_col(13.into()); // LineCol { line: 1, col: 3 } line_index.offset(LineCol { line: 2, col: 3 }); // Some (24) ``` ## SemVer This crate uses [semver](https://semver.org/) versioning. line-index-0.1.2/src/lib.rs000064400000000000000000000425761046102023000136010ustar 00000000000000//! See [`LineIndex`]. #![deny(missing_debug_implementations, missing_docs, rust_2018_idioms)] #[cfg(test)] mod tests; use nohash_hasher::IntMap; pub use text_size::{TextRange, TextSize}; /// `(line, column)` information in the native, UTF-8 encoding. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct LineCol { /// Zero-based. pub line: u32, /// Zero-based UTF-8 offset. pub col: u32, } /// A kind of wide character encoding. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum WideEncoding { /// UTF-16. Utf16, /// UTF-32. Utf32, } impl WideEncoding { /// Returns the number of code units it takes to encode `text` in this encoding. pub fn measure(&self, text: &str) -> usize { match self { WideEncoding::Utf16 => text.encode_utf16().count(), WideEncoding::Utf32 => text.chars().count(), } } } /// `(line, column)` information in wide encodings. /// /// See [`WideEncoding`] for the kinds of wide encodings available. // // Deliberately not a generic type and different from `LineCol`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct WideLineCol { /// Zero-based. pub line: u32, /// Zero-based. pub col: u32, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct WideChar { /// Start offset of a character inside a line, zero-based. start: TextSize, /// End offset of a character inside a line, zero-based. end: TextSize, } impl WideChar { /// Returns the length in 8-bit UTF-8 code units. fn len(&self) -> TextSize { self.end - self.start } /// Returns the length in UTF-16 or UTF-32 code units. fn wide_len(&self, enc: WideEncoding) -> u32 { match enc { WideEncoding::Utf16 => { if self.len() == TextSize::from(4) { 2 } else { 1 } } WideEncoding::Utf32 => 1, } } } /// Maps flat [`TextSize`] offsets to/from `(line, column)` representation. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LineIndex { /// Offset the beginning of each line (except the first, which always has offset 0). newlines: Box<[TextSize]>, /// List of non-ASCII characters on each line. line_wide_chars: IntMap>, /// The length of the entire text. len: TextSize, } impl LineIndex { /// Returns a `LineIndex` for the `text`. pub fn new(text: &str) -> LineIndex { let (newlines, line_wide_chars) = analyze_source_file(text); LineIndex { newlines: newlines.into_boxed_slice(), line_wide_chars, len: TextSize::of(text), } } /// Transforms the `TextSize` into a `LineCol`. /// /// # Panics /// /// If the offset is invalid. See [`Self::try_line_col`]. pub fn line_col(&self, offset: TextSize) -> LineCol { self.try_line_col(offset).expect("invalid offset") } /// Transforms the `TextSize` into a `LineCol`. /// /// Returns `None` if the `offset` was invalid, e.g. if it extends past the end of the text or /// points to the middle of a multi-byte character. pub fn try_line_col(&self, offset: TextSize) -> Option { if offset > self.len { return None; } let line = self.newlines.partition_point(|&it| it <= offset); let start = self.start_offset(line)?; let col = offset - start; let ret = LineCol { line: line as u32, col: col.into() }; self.line_wide_chars .get(&ret.line) .into_iter() .flat_map(|it| it.iter()) .all(|it| col <= it.start || it.end <= col) .then_some(ret) } /// Transforms the `LineCol` into a `TextSize`. pub fn offset(&self, line_col: LineCol) -> Option { self.start_offset(line_col.line as usize).map(|start| start + TextSize::from(line_col.col)) } fn start_offset(&self, line: usize) -> Option { match line.checked_sub(1) { None => Some(TextSize::from(0)), Some(it) => self.newlines.get(it).copied(), } } /// Transforms the `LineCol` with the given `WideEncoding` into a `WideLineCol`. pub fn to_wide(&self, enc: WideEncoding, line_col: LineCol) -> Option { let mut col = line_col.col; if let Some(wide_chars) = self.line_wide_chars.get(&line_col.line) { for c in wide_chars.iter() { if u32::from(c.end) <= line_col.col { col = col.checked_sub(u32::from(c.len()) - c.wide_len(enc))?; } else { // From here on, all utf16 characters come *after* the character we are mapping, // so we don't need to take them into account break; } } } Some(WideLineCol { line: line_col.line, col }) } /// Transforms the `WideLineCol` with the given `WideEncoding` into a `LineCol`. pub fn to_utf8(&self, enc: WideEncoding, line_col: WideLineCol) -> Option { let mut col = line_col.col; if let Some(wide_chars) = self.line_wide_chars.get(&line_col.line) { for c in wide_chars.iter() { if col > u32::from(c.start) { col = col.checked_add(u32::from(c.len()) - c.wide_len(enc))?; } else { // From here on, all utf16 characters come *after* the character we are mapping, // so we don't need to take them into account break; } } } Some(LineCol { line: line_col.line, col }) } /// Returns the given line's range. pub fn line(&self, line: u32) -> Option { let start = self.start_offset(line as usize)?; let next_newline = self.newlines.get(line as usize).copied().unwrap_or(self.len); let line_length = next_newline - start; Some(TextRange::new(start, start + line_length)) } /// Given a range [start, end), returns a sorted iterator of non-empty ranges [start, x1), [x1, /// x2), ..., [xn, end) where all the xi, which are positions of newlines, are inside the range /// [start, end). pub fn lines(&self, range: TextRange) -> impl Iterator + '_ { let lo = self.newlines.partition_point(|&it| it < range.start()); let hi = self.newlines.partition_point(|&it| it <= range.end()); let all = std::iter::once(range.start()) .chain(self.newlines[lo..hi].iter().copied()) .chain(std::iter::once(range.end())); all.clone() .zip(all.skip(1)) .map(|(lo, hi)| TextRange::new(lo, hi)) .filter(|it| !it.is_empty()) } /// Returns the length of the original text. pub fn len(&self) -> TextSize { self.len } } /// This is adapted from the rustc_span crate, https://github.com/rust-lang/rust/blob/de59844c98f7925242a798a72c59dc3610dd0e2c/compiler/rustc_span/src/analyze_source_file.rs fn analyze_source_file(src: &str) -> (Vec, IntMap>) { assert!(src.len() < !0u32 as usize); let mut lines = vec![]; let mut line_wide_chars = IntMap::>::default(); // Calls the right implementation, depending on hardware support available. analyze_source_file_dispatch(src, &mut lines, &mut line_wide_chars); (lines, line_wide_chars.into_iter().map(|(k, v)| (k, v.into_boxed_slice())).collect()) } #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] fn analyze_source_file_dispatch( src: &str, lines: &mut Vec, multi_byte_chars: &mut IntMap>, ) { if is_x86_feature_detected!("sse2") { // SAFETY: SSE2 support was checked unsafe { analyze_source_file_sse2(src, lines, multi_byte_chars); } } else { analyze_source_file_generic(src, src.len(), TextSize::from(0), lines, multi_byte_chars); } } #[cfg(target_arch = "aarch64")] fn analyze_source_file_dispatch( src: &str, lines: &mut Vec, multi_byte_chars: &mut IntMap>, ) { if std::arch::is_aarch64_feature_detected!("neon") { // SAFETY: NEON support was checked unsafe { analyze_source_file_neon(src, lines, multi_byte_chars); } } else { analyze_source_file_generic(src, src.len(), TextSize::from(0), lines, multi_byte_chars); } } /// Checks 16 byte chunks of text at a time. If the chunk contains /// something other than printable ASCII characters and newlines, the /// function falls back to the generic implementation. Otherwise it uses /// SSE2 intrinsics to quickly find all newlines. #[target_feature(enable = "sse2")] #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] unsafe fn analyze_source_file_sse2( src: &str, lines: &mut Vec, multi_byte_chars: &mut IntMap>, ) { #[cfg(target_arch = "x86")] use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; const CHUNK_SIZE: usize = 16; let src_bytes = src.as_bytes(); let chunk_count = src.len() / CHUNK_SIZE; // This variable keeps track of where we should start decoding a // chunk. If a multi-byte character spans across chunk boundaries, // we need to skip that part in the next chunk because we already // handled it. let mut intra_chunk_offset = 0; for chunk_index in 0..chunk_count { let ptr = src_bytes.as_ptr() as *const __m128i; // We don't know if the pointer is aligned to 16 bytes, so we // use `loadu`, which supports unaligned loading. let chunk = unsafe { _mm_loadu_si128(ptr.add(chunk_index)) }; // For character in the chunk, see if its byte value is < 0, which // indicates that it's part of a UTF-8 char. let multibyte_test = unsafe { _mm_cmplt_epi8(chunk, _mm_set1_epi8(0)) }; // Create a bit mask from the comparison results. let multibyte_mask = unsafe { _mm_movemask_epi8(multibyte_test) }; // If the bit mask is all zero, we only have ASCII chars here: if multibyte_mask == 0 { assert!(intra_chunk_offset == 0); // Check for newlines in the chunk let newlines_test = unsafe { _mm_cmpeq_epi8(chunk, _mm_set1_epi8(b'\n' as i8)) }; let newlines_mask = unsafe { _mm_movemask_epi8(newlines_test) }; if newlines_mask != 0 { // All control characters are newlines, record them let mut newlines_mask = 0xFFFF0000 | newlines_mask as u32; let output_offset = TextSize::from((chunk_index * CHUNK_SIZE + 1) as u32); loop { let index = newlines_mask.trailing_zeros(); if index >= CHUNK_SIZE as u32 { // We have arrived at the end of the chunk. break; } lines.push(TextSize::from(index) + output_offset); // Clear the bit, so we can find the next one. newlines_mask &= (!1) << index; } } continue; } // The slow path. // There are control chars in here, fallback to generic decoding. let scan_start = chunk_index * CHUNK_SIZE + intra_chunk_offset; intra_chunk_offset = analyze_source_file_generic( &src[scan_start..], CHUNK_SIZE - intra_chunk_offset, TextSize::from(scan_start as u32), lines, multi_byte_chars, ); } // There might still be a tail left to analyze let tail_start = chunk_count * CHUNK_SIZE + intra_chunk_offset; if tail_start < src.len() { analyze_source_file_generic( &src[tail_start..], src.len() - tail_start, TextSize::from(tail_start as u32), lines, multi_byte_chars, ); } } #[target_feature(enable = "neon")] #[cfg(target_arch = "aarch64")] #[inline] // See https://community.arm.com/arm-community-blogs/b/infrastructure-solutions-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon // // The mask is a 64-bit integer, where each 4-bit corresponds to a u8 in the // input vector. The least significant 4 bits correspond to the first byte in // the vector. unsafe fn move_mask(v: std::arch::aarch64::uint8x16_t) -> u64 { use std::arch::aarch64::*; let nibble_mask = unsafe { vshrn_n_u16(vreinterpretq_u16_u8(v), 4) }; unsafe { vget_lane_u64(vreinterpret_u64_u8(nibble_mask), 0) } } #[target_feature(enable = "neon")] #[cfg(target_arch = "aarch64")] unsafe fn analyze_source_file_neon( src: &str, lines: &mut Vec, multi_byte_chars: &mut IntMap>, ) { use std::arch::aarch64::*; const CHUNK_SIZE: usize = 16; let src_bytes = src.as_bytes(); let chunk_count = src.len() / CHUNK_SIZE; let newline = unsafe { vdupq_n_s8(b'\n' as i8) }; // This variable keeps track of where we should start decoding a // chunk. If a multi-byte character spans across chunk boundaries, // we need to skip that part in the next chunk because we already // handled it. let mut intra_chunk_offset = 0; for chunk_index in 0..chunk_count { let ptr = src_bytes.as_ptr() as *const i8; let chunk = unsafe { vld1q_s8(ptr.add(chunk_index * CHUNK_SIZE)) }; // For character in the chunk, see if its byte value is < 0, which // indicates that it's part of a UTF-8 char. let multibyte_test = unsafe { vcltzq_s8(chunk) }; // Create a bit mask from the comparison results. let multibyte_mask = unsafe { move_mask(multibyte_test) }; // If the bit mask is all zero, we only have ASCII chars here: if multibyte_mask == 0 { assert!(intra_chunk_offset == 0); // Check for newlines in the chunk let newlines_test = unsafe { vceqq_s8(chunk, newline) }; let mut newlines_mask = unsafe { move_mask(newlines_test) }; // If the bit mask is not all zero, there are newlines in this chunk. if newlines_mask != 0 { let output_offset = TextSize::from((chunk_index * CHUNK_SIZE + 1) as u32); while newlines_mask != 0 { let trailing_zeros = newlines_mask.trailing_zeros(); let index = trailing_zeros / 4; lines.push(TextSize::from(index) + output_offset); // Clear the current 4-bit, so we can find the next one. newlines_mask &= (!0xF) << trailing_zeros; } } continue; } let scan_start = chunk_index * CHUNK_SIZE + intra_chunk_offset; intra_chunk_offset = analyze_source_file_generic( &src[scan_start..], CHUNK_SIZE - intra_chunk_offset, TextSize::from(scan_start as u32), lines, multi_byte_chars, ); } let tail_start = chunk_count * CHUNK_SIZE + intra_chunk_offset; if tail_start < src.len() { analyze_source_file_generic( &src[tail_start..], src.len() - tail_start, TextSize::from(tail_start as u32), lines, multi_byte_chars, ); } } #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))] // The target (or compiler version) does not support SSE2 ... fn analyze_source_file_dispatch( src: &str, lines: &mut Vec, multi_byte_chars: &mut IntMap>, ) { analyze_source_file_generic(src, src.len(), TextSize::from(0), lines, multi_byte_chars); } // `scan_len` determines the number of bytes in `src` to scan. Note that the // function can read past `scan_len` if a multi-byte character start within the // range but extends past it. The overflow is returned by the function. fn analyze_source_file_generic( src: &str, scan_len: usize, output_offset: TextSize, lines: &mut Vec, multi_byte_chars: &mut IntMap>, ) -> usize { assert!(src.len() >= scan_len); let mut i = 0; let src_bytes = src.as_bytes(); while i < scan_len { let byte = unsafe { // We verified that i < scan_len <= src.len() *src_bytes.get_unchecked(i) }; // How much to advance in order to get to the next UTF-8 char in the // string. let mut char_len = 1; if byte == b'\n' { lines.push(TextSize::from(i as u32 + 1) + output_offset); } else if byte >= 127 { // The slow path: Just decode to `char`. let c = src[i..].chars().next().unwrap(); char_len = c.len_utf8(); // The last element of `lines` represents the offset of the start of // current line. To get the offset inside the line, we subtract it. let pos = TextSize::from(i as u32) + output_offset - lines.last().unwrap_or(&TextSize::default()); if char_len > 1 { assert!((2..=4).contains(&char_len)); let mbc = WideChar { start: pos, end: pos + TextSize::from(char_len as u32) }; multi_byte_chars.entry(lines.len() as u32).or_default().push(mbc); } } i += char_len; } i - scan_len } line-index-0.1.2/src/tests.rs000064400000000000000000000140571046102023000141660ustar 00000000000000use crate::{LineCol, LineIndex, TextSize, WideChar, WideEncoding, WideLineCol}; macro_rules! test { ( case: $test_name:ident, text: $text:expr, lines: $lines:expr, multi_byte_chars: $multi_byte_chars:expr, ) => { #[test] fn $test_name() { let line_index = LineIndex::new($text); let expected_lines: Vec = $lines.into_iter().map(>::from).collect(); assert_eq!(&*line_index.newlines, &*expected_lines); let expected_mbcs: Vec<_> = $multi_byte_chars .into_iter() .map(|(line, (pos, end)): (u32, (u32, u32))| { (line, WideChar { start: TextSize::from(pos), end: TextSize::from(end) }) }) .collect(); assert_eq!( line_index .line_wide_chars .iter() .flat_map(|(line, val)| std::iter::repeat(*line).zip(val.iter().copied())) .collect::>(), expected_mbcs ); } }; } test!( case: empty_text, text: "", lines: vec![], multi_byte_chars: vec![], ); test!( case: newlines_short, text: "a\nc", lines: vec![2], multi_byte_chars: vec![], ); test!( case: newlines_long, text: "012345678\nabcdef012345678\na", lines: vec![10, 26], multi_byte_chars: vec![], ); test!( case: newline_and_multi_byte_char_in_same_chunk, text: "01234β789\nbcdef0123456789abcdef", lines: vec![11], multi_byte_chars: vec![(0, (5, 7))], ); test!( case: newline_and_control_char_in_same_chunk, text: "01234\u{07}6789\nbcdef0123456789abcdef", lines: vec![11], multi_byte_chars: vec![], ); test!( case: multi_byte_char_short, text: "aβc", lines: vec![], multi_byte_chars: vec![(0, (1, 3))], ); test!( case: multi_byte_char_long, text: "0123456789abcΔf012345β", lines: vec![], multi_byte_chars: vec![(0, (13, 15)), (0, (22, 24))], ); test!( case: multi_byte_char_across_chunk_boundary, text: "0123456789abcdeΔ123456789abcdef01234", lines: vec![], multi_byte_chars: vec![(0, (15, 17))], ); test!( case: multi_byte_char_across_chunk_boundary_tail, text: "0123456789abcdeΔ....", lines: vec![], multi_byte_chars: vec![(0, (15, 17))], ); test!( case: multi_byte_with_new_lines, text: "01\t345\n789abcΔf01234567\u{07}9\nbcΔf", lines: vec![7, 27], multi_byte_chars: vec![(1, (6, 8)), (2, (2, 4))], ); test!( case: trailing_newline, text: "0123456789\n", lines: vec![11], multi_byte_chars: vec![], ); test!( case: trailing_newline_chunk_boundary, text: "0123456789abcde\n", lines: vec![16], multi_byte_chars: vec![], ); #[test] fn test_try_line_col() { let text = "\n\n\n\n\n宽3456"; assert_eq!(&text[5..8], "宽"); assert_eq!(&text[11..12], "6"); let line_index = LineIndex::new(text); let before_6 = TextSize::from(11); let line_col = line_index.try_line_col(before_6); assert_eq!(line_col, Some(LineCol { line: 5, col: 6 })); } #[test] fn test_to_wide() { let text = "\n\n\n\n\n宽3456"; assert_eq!(&text[5..8], "宽"); assert_eq!(&text[11..12], "6"); let line_index = LineIndex::new(text); let before_6 = TextSize::from(11); let line_col = line_index.try_line_col(before_6); assert_eq!(line_col, Some(LineCol { line: 5, col: 6 })); let wide_line_col = line_index.to_wide(WideEncoding::Utf16, line_col.unwrap()); assert_eq!(wide_line_col, Some(WideLineCol { line: 5, col: 4 })); } #[test] fn test_every_chars() { let text: String = { let mut chars: Vec = ((0 as char)..char::MAX).collect(); // Neat! chars.extend("\n".repeat(chars.len() / 16).chars()); let seed = std::hash::Hasher::finish(&std::hash::BuildHasher::build_hasher( #[allow(clippy::disallowed_types)] &std::collections::hash_map::RandomState::new(), )); let mut rng = oorandom::Rand32::new(seed); let mut rand_index = |i| rng.rand_range(0..i as u32) as usize; let mut remaining = chars.len() - 1; while remaining > 0 { let index = rand_index(remaining); chars.swap(remaining, index); remaining -= 1; } chars.into_iter().collect() }; assert!(text.contains('💩')); // Sanity check. let line_index = LineIndex::new(&text); let mut lin_col = LineCol { line: 0, col: 0 }; let mut col_utf16 = 0; let mut col_utf32 = 0; for (offset, c) in text.char_indices() { let got_offset = line_index.offset(lin_col).unwrap(); assert_eq!(usize::from(got_offset), offset); let got_lin_col = line_index.line_col(got_offset); assert_eq!(got_lin_col, lin_col); for (enc, col) in [(WideEncoding::Utf16, col_utf16), (WideEncoding::Utf32, col_utf32)] { let wide_lin_col = line_index.to_wide(enc, lin_col).unwrap(); let got_lin_col = line_index.to_utf8(enc, wide_lin_col).unwrap(); assert_eq!(got_lin_col, lin_col); assert_eq!(wide_lin_col.col, col) } if c == '\n' { lin_col.line += 1; lin_col.col = 0; col_utf16 = 0; col_utf32 = 0; } else { lin_col.col += c.len_utf8() as u32; col_utf16 += c.len_utf16() as u32; col_utf32 += 1; } } } #[test] fn test_line() { use text_size::TextRange; macro_rules! validate { ($text:expr, $line:expr, $expected_start:literal .. $expected_end:literal) => { let line_index = LineIndex::new($text); assert_eq!( line_index.line($line), Some(TextRange::new( TextSize::from($expected_start), TextSize::from($expected_end) )) ); }; } validate!("", 0, 0..0); validate!("\n", 1, 1..1); validate!("\nabc", 1, 1..4); validate!("\nabc\ndef", 1, 1..5); }