use tree_sitter::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SourceRow(usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SourceColumn(usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SourceOffset(usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct ScreenColumn(usize);
macro_rules! position_newtype {
($name:ident) => {
impl $name {
pub const fn from_raw(value: usize) -> Self {
Self(value)
}
pub const fn get(self) -> usize {
self.0
}
}
};
}
position_newtype!(SourceRow);
position_newtype!(SourceColumn);
position_newtype!(SourceOffset);
position_newtype!(ScreenColumn);
pub fn row_len_of(line: &str) -> SourceColumn {
SourceColumn(line.len())
}
pub fn paint_row_len(line: &str) -> SourceColumn {
row_len_of(line.trim_end())
}
pub fn floor_char_boundary(line: &str, column: usize) -> usize {
let mut column = column.min(line.len());
while column > 0 && !line.is_char_boundary(column) {
column -= 1;
}
column
}
pub fn screen_column_in(line: &str, column: SourceColumn) -> ScreenColumn {
let mut cells = 0usize;
for (index, ch) in line.char_indices() {
if index + ch.len_utf8() > column.get() {
break;
}
cells += cell_width_of(ch).get();
}
ScreenColumn(cells)
}
pub fn cell_width_of(ch: char) -> ScreenColumn {
use unicode_width::UnicodeWidthChar;
ScreenColumn(ch.width().unwrap_or(0))
}
pub fn row_cells_of(line: &str) -> ScreenColumn {
screen_column_in(line, row_len_of(line))
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextRange {
pub start_row: usize,
pub start_column: usize,
pub end_row: usize,
pub end_column: usize,
}
impl TextRange {
pub fn new(start_row: usize, start_column: usize, end_row: usize, end_column: usize) -> Self {
Self {
start_row,
start_column,
end_row,
end_column,
}
}
pub fn right_limit(&self) -> Self {
Self {
start_row: self.end_row,
start_column: self.end_column,
end_row: self.end_row,
end_column: self.end_column,
}
}
pub fn zero() -> Self {
Self {
start_row: 0,
start_column: 0,
end_row: 0,
end_column: 0,
}
}
pub fn is_zero(&self) -> bool {
self.start_row == 0 && self.start_column == 0 && self.end_row == 0 && self.end_column == 0
}
pub fn is_empty(&self) -> bool {
self.start_row == self.end_row && self.start_column == self.end_column
}
pub fn from_treesitter_range(ts_range: Range, columns_per_row: &[usize]) -> Self {
let mut end_row = ts_range.end_point.row;
let mut end_column = ts_range.end_point.column;
if end_row < columns_per_row.len() && columns_per_row[end_row] == end_column {
end_row += 1;
end_column = 0;
}
Self {
start_row: ts_range.start_point.row,
start_column: ts_range.start_point.column,
end_row,
end_column,
}
}
pub fn intersects(&self, other: &TextRange) -> bool {
let self_start = (self.start_row, self.start_column);
let self_end = (self.end_row, self.end_column);
let other_start = (other.start_row, other.start_column);
let other_end = (other.end_row, other.end_column);
(self_start == self_end && other_start == other_end && self_start == other_start)
|| (self_start < other_end && other_start < self_end)
}
pub fn extends(&self, other: &TextRange) -> bool {
self.start_row == other.end_row && self.start_column == other.end_column
}
pub fn extend_to_end(&mut self, other: &TextRange) {
self.end_row = other.end_row;
self.end_column = other.end_column;
}
pub fn can_extend_with_whitespace(&self, other: &TextRange, code: &SourceText) -> bool {
is_whitespace_between(self, other, code)
}
pub fn columns_on_row(&self, row: usize, row_len: SourceColumn) -> Option<(usize, usize)> {
let row_len = row_len.get();
if row < self.start_row || row > self.end_row {
return None;
}
let start_col = if row == self.start_row {
self.start_column
} else {
0
};
let end_col = if row == self.end_row {
self.end_column
} else {
row_len
};
if start_col >= end_col {
return None;
}
Some((start_col, end_col))
}
}
fn is_whitespace_between(a: &TextRange, b: &TextRange, code: &SourceText) -> bool {
if a.extends(b) {
return true;
}
if b.extends(a) {
return true;
}
let a_end_pos = (a.end_row, a.end_column);
let b_start_pos = (b.start_row, b.start_column);
let b_end_pos = (b.end_row, b.end_column);
let a_start_pos = (a.start_row, a.start_column);
let (first, second) = if a_end_pos <= b_start_pos {
(a, b)
} else if b_end_pos <= a_start_pos {
(b, a)
} else {
return false;
};
let (Some(first_end_idx), Some(second_start_idx)) = (
code.byte_index(
SourceRow::from_raw(first.end_row),
SourceColumn::from_raw(first.end_column),
),
code.byte_index(
SourceRow::from_raw(second.start_row),
SourceColumn::from_raw(second.start_column),
),
) else {
return false;
};
if first_end_idx >= second_start_idx {
return true;
}
let gap_text = &code.text()[first_end_idx.get()..second_start_idx.get()];
gap_text.chars().all(|c| c.is_whitespace())
}
pub struct SourceText<'a> {
text: &'a str,
row_starts: Vec<usize>,
}
impl<'a> SourceText<'a> {
pub fn new(text: &'a str) -> Self {
let mut row_starts = vec![0usize];
row_starts.extend(
text.bytes()
.enumerate()
.filter(|(_, b)| *b == b'\n')
.map(|(i, _)| i + 1),
);
SourceText { text, row_starts }
}
pub fn text(&self) -> &'a str {
self.text
}
pub fn byte_index(&self, row: SourceRow, column: SourceColumn) -> Option<SourceOffset> {
if row.get() == self.row_starts.len() && column.get() == 0 {
return Some(SourceOffset::from_raw(self.text.len()));
}
let &start = self.row_starts.get(row.get())?;
let row_end = self
.row_starts
.get(row.get() + 1)
.map(|next| next - 1)
.unwrap_or(self.text.len());
let index = start + column.get();
if index > row_end || !self.text.is_char_boundary(index) {
return None;
}
Some(SourceOffset::from_raw(index))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn row_col_to_byte_index(row: usize, col: usize, code: &str) -> Option<usize> {
if row == code.split('\n').count() && col == 0 {
return Some(code.len());
}
let mut current_row = 0;
let mut current_col = 0; let mut byte_index = 0;
for ch in code.chars() {
if current_row == row && current_col == col {
return Some(byte_index);
}
let len = ch.len_utf8();
if ch == '\n' {
current_row += 1;
current_col = 0;
} else {
current_col += len;
}
byte_index += len;
}
(current_row == row && current_col == col).then_some(byte_index)
}
#[test]
#[ignore = "slow: reads the whole fixture corpus"]
fn byte_index_agrees_with_a_linear_walk_on_the_corpus() {
let pairs = crate::test::helper::handmade_test_code_pairs().expect("corpus");
let mut checked = 0usize;
for (name, (before, after)) in pairs.iter() {
for code in [&before.contents, &after.contents] {
let index = SourceText::new(code);
let rows = code.split('\n').count();
for row in 0..=rows {
let width = code.split('\n').nth(row).map(str::len).unwrap_or(0);
for column in 0..=(width + 2) {
assert_eq!(
index
.byte_index(
SourceRow::from_raw(row),
SourceColumn::from_raw(column)
)
.map(SourceOffset::get),
row_col_to_byte_index(row, column, code),
"'{name}' ({row}, {column})"
);
}
}
checked += 1;
}
}
assert!(checked > 100, "only checked {checked} files");
}
#[test]
fn byte_index_agrees_with_a_linear_walk_everywhere() {
let samples = [
"",
"\n",
"a",
"a\n",
"one\ntwo\nthree\n",
"no trailing newline\nsecond",
"a\u{2014}b c", "\u{1F600}x\n\u{00E9}\u{00E9}\n\n tail", " \t \n\t\n ", ];
for code in samples {
let index = SourceText::new(code);
let rows = code.split('\n').count() + 2;
for row in 0..rows {
for column in 0..(code.len() + 3) {
assert_eq!(
index
.byte_index(SourceRow::from_raw(row), SourceColumn::from_raw(column))
.map(SourceOffset::get),
row_col_to_byte_index(row, column, code),
"({row}, {column}) in {code:?}"
);
}
}
}
}
#[test]
fn is_zero_recognizes_only_the_origin_sentinel_while_is_empty_holds_anywhere() {
let elsewhere = TextRange::new(3, 4, 3, 4);
assert!(elsewhere.is_empty());
assert!(!elsewhere.is_zero());
assert!(TextRange::zero().is_zero());
assert!(TextRange::zero().is_empty());
}
#[test]
fn columns_on_row_spans_a_middle_row_whole_and_skips_rows_outside() {
let range = TextRange::new(1, 4, 3, 2);
assert_eq!(range.columns_on_row(0, SourceColumn::from_raw(9)), None);
assert_eq!(
range.columns_on_row(1, SourceColumn::from_raw(9)),
Some((4, 9))
);
assert_eq!(
range.columns_on_row(2, SourceColumn::from_raw(7)),
Some((0, 7))
);
assert_eq!(
range.columns_on_row(3, SourceColumn::from_raw(9)),
Some((0, 2))
);
assert_eq!(range.columns_on_row(4, SourceColumn::from_raw(9)), None);
assert_eq!(
TextRange::new(0, 0, 1, 0).columns_on_row(1, SourceColumn::from_raw(5)),
None
);
}
#[test]
fn text_range_intersects_overlapping() {
let a = TextRange::new(0, 0, 5, 0);
let b = TextRange::new(3, 0, 8, 0);
assert!(a.intersects(&b));
assert!(b.intersects(&a));
}
#[test]
fn text_range_intersects_touching() {
let a = TextRange::new(0, 0, 5, 0);
let b = TextRange::new(5, 0, 10, 0);
assert!(!a.intersects(&b));
assert!(!b.intersects(&a));
}
#[test]
fn text_range_intersects_identical() {
let a = TextRange::new(0, 0, 5, 0);
let b = TextRange::new(0, 0, 5, 0);
assert!(a.intersects(&b));
}
#[test]
fn text_range_intersects_contains() {
let a = TextRange::new(0, 0, 10, 0);
let b = TextRange::new(2, 0, 5, 0);
assert!(a.intersects(&b));
assert!(b.intersects(&a));
}
#[test]
fn text_range_intersects_disjoint() {
let a = TextRange::new(0, 0, 2, 0);
let b = TextRange::new(5, 0, 8, 0);
assert!(!a.intersects(&b));
assert!(!b.intersects(&a));
}
#[test]
fn text_range_intersects_same_row_different_columns() {
let a = TextRange::new(0, 0, 0, 10);
let b = TextRange::new(0, 5, 0, 15);
assert!(a.intersects(&b));
assert!(b.intersects(&a));
}
#[test]
fn text_range_intersects_same_row_touching_columns() {
let a = TextRange::new(0, 0, 0, 10);
let b = TextRange::new(0, 10, 0, 20);
assert!(!a.intersects(&b));
assert!(!b.intersects(&a));
}
#[test]
fn text_range_intersects_empty_range() {
let a = TextRange::new(0, 0, 5, 0);
let b = TextRange::new(2, 0, 2, 0);
assert!(a.intersects(&b));
assert!(b.intersects(&a));
}
#[test]
fn text_range_intersects_empty_ranges_same_point() {
let a = TextRange::new(0, 0, 0, 0);
let b = TextRange::new(0, 0, 0, 0);
assert!(a.intersects(&b));
}
#[test]
fn text_range_intersects_crossing_rows() {
let a = TextRange::new(0, 5, 2, 5);
let b = TextRange::new(1, 0, 3, 0);
assert!(a.intersects(&b));
assert!(b.intersects(&a));
}
#[test]
fn from_treesitter_range_does_not_normalize_a_mid_row_column_that_matches_a_character_count() {
use tree_sitter::{Point, Range};
let row = "let æ¼¢ = \"yy\";";
assert_eq!(row.len(), 15, "fifteen bytes");
assert_eq!(row.chars().count(), 13, "thirteen characters");
let row_byte_lengths = crate::code::metadata::compute_row_byte_lengths(row);
let mid_row = TextRange::from_treesitter_range(
Range {
start_point: Point { row: 0, column: 11 },
end_point: Point { row: 0, column: 13 },
start_byte: 11,
end_byte: 13,
},
&row_byte_lengths,
);
assert_eq!(
(mid_row.end_row, mid_row.end_column),
(0, 13),
"byte column 13 is mid-row here and must stay where it is"
);
let end_of_row = TextRange::from_treesitter_range(
Range {
start_point: Point { row: 0, column: 11 },
end_point: Point { row: 0, column: 15 },
start_byte: 11,
end_byte: 15,
},
&row_byte_lengths,
);
assert_eq!(
(end_of_row.end_row, end_of_row.end_column),
(1, 0),
"the row's real end still normalises"
);
}
#[test]
fn from_treesitter_range_end_at_line_end() {
use tree_sitter::Point;
use tree_sitter::Range;
let ts_range = Range {
start_point: Point { row: 0, column: 0 },
end_point: Point { row: 0, column: 5 },
start_byte: 0,
end_byte: 5,
};
let columns_per_row = vec![5];
let result = TextRange::from_treesitter_range(ts_range, &columns_per_row);
assert_eq!(result.start_row, 0);
assert_eq!(result.start_column, 0);
assert_eq!(result.end_row, 1);
assert_eq!(result.end_column, 0);
}
#[test]
fn from_treesitter_range_end_not_at_line_end() {
use tree_sitter::Point;
use tree_sitter::Range;
let ts_range = Range {
start_point: Point { row: 0, column: 0 },
end_point: Point { row: 0, column: 3 },
start_byte: 0,
end_byte: 3,
};
let columns_per_row = vec![5];
let result = TextRange::from_treesitter_range(ts_range, &columns_per_row);
assert_eq!(result.start_row, 0);
assert_eq!(result.start_column, 0);
assert_eq!(result.end_row, 0);
assert_eq!(result.end_column, 3);
}
#[test]
fn from_treesitter_range_multiline() {
use tree_sitter::Point;
use tree_sitter::Range;
let ts_range = Range {
start_point: Point { row: 0, column: 0 },
end_point: Point { row: 1, column: 3 }, start_byte: 0,
end_byte: 7,
};
let columns_per_row = vec![5, 5];
let result = TextRange::from_treesitter_range(ts_range, &columns_per_row);
assert_eq!(result.start_row, 0);
assert_eq!(result.start_column, 0);
assert_eq!(result.end_row, 1);
assert_eq!(result.end_column, 3);
}
#[test]
fn from_treesitter_range_end_at_last_line_end() {
use tree_sitter::Point;
use tree_sitter::Range;
let ts_range = Range {
start_point: Point { row: 0, column: 0 },
end_point: Point { row: 1, column: 5 }, start_byte: 0,
end_byte: 11,
};
let columns_per_row = vec![5, 5];
let result = TextRange::from_treesitter_range(ts_range, &columns_per_row);
assert_eq!(result.start_row, 0);
assert_eq!(result.start_column, 0);
assert_eq!(result.end_row, 2);
assert_eq!(result.end_column, 0);
}
#[test]
fn from_treesitter_range_empty_range() {
use tree_sitter::Point;
use tree_sitter::Range;
let ts_range = Range {
start_point: Point { row: 0, column: 2 },
end_point: Point { row: 0, column: 2 },
start_byte: 2,
end_byte: 2,
};
let columns_per_row = vec![5];
let result = TextRange::from_treesitter_range(ts_range, &columns_per_row);
assert_eq!(result.start_row, 0);
assert_eq!(result.start_column, 2);
assert_eq!(result.end_row, 0);
assert_eq!(result.end_column, 2);
}
#[test]
fn from_treesitter_range_end_row_beyond_columns() {
use tree_sitter::Point;
use tree_sitter::Range;
let ts_range = Range {
start_point: Point { row: 0, column: 0 },
end_point: Point { row: 2, column: 0 }, start_byte: 0,
end_byte: 0,
};
let columns_per_row = vec![5, 5];
let result = TextRange::from_treesitter_range(ts_range, &columns_per_row);
assert_eq!(result.start_row, 0);
assert_eq!(result.start_column, 0);
assert_eq!(result.end_row, 2);
assert_eq!(result.end_column, 0);
}
#[test]
fn from_treesitter_range_end_row_beyond_columns_nonzero_column() {
use tree_sitter::Point;
use tree_sitter::Range;
let ts_range = Range {
start_point: Point { row: 0, column: 0 },
end_point: Point { row: 2, column: 3 }, start_byte: 0,
end_byte: 0,
};
let columns_per_row = vec![5, 5];
let result = TextRange::from_treesitter_range(ts_range, &columns_per_row);
assert_eq!(result.start_row, 0);
assert_eq!(result.start_column, 0);
assert_eq!(result.end_row, 2);
assert_eq!(result.end_column, 3);
}
#[test]
fn text_range_can_extend_exact_touch() {
let a = TextRange::new(0, 0, 1, 0);
let b = TextRange::new(1, 0, 2, 0);
let code = "line1\nline2\nline3";
assert!(a.can_extend_with_whitespace(&b, &SourceText::new(code)));
}
#[test]
fn text_range_can_extend_with_whitespace_only() {
let a = TextRange::new(0, 0, 1, 0);
let b = TextRange::new(1, 0, 2, 0);
let code = "line1\n \nline3";
assert!(a.can_extend_with_whitespace(&b, &SourceText::new(code)));
}
#[test]
fn text_range_cannot_extend_with_non_whitespace() {
let a = TextRange::new(0, 0, 0, 5);
let b = TextRange::new(0, 7, 0, 10);
let code = "helloXworld";
assert!(!a.can_extend_with_whitespace(&b, &SourceText::new(code)));
}
#[test]
fn text_range_can_extend_same_line_with_spaces() {
let a = TextRange::new(0, 0, 0, 5);
let b = TextRange::new(0, 7, 0, 10);
let code = "hello world";
assert!(a.can_extend_with_whitespace(&b, &SourceText::new(code)));
}
#[test]
fn text_range_cannot_extend_same_line_with_text() {
let a = TextRange::new(0, 0, 0, 5);
let b = TextRange::new(0, 7, 0, 10);
let code = "helloXworld";
assert!(!a.can_extend_with_whitespace(&b, &SourceText::new(code)));
}
#[test]
fn text_range_cannot_extend_multi_line_with_non_whitespace() {
let a = TextRange::new(0, 0, 1, 0);
let b = TextRange::new(2, 0, 3, 0);
let code = "line1\ntext\nline3";
assert!(!a.can_extend_with_whitespace(&b, &SourceText::new(code)));
}
#[test]
fn row_col_to_byte_index_lands_after_a_multi_byte_character() {
let code = "a—bc";
assert_eq!(row_col_to_byte_index(0, 0, code), Some(0)); assert_eq!(row_col_to_byte_index(0, 1, code), Some(1)); assert_eq!(row_col_to_byte_index(0, 4, code), Some(4)); assert_eq!(row_col_to_byte_index(0, 5, code), Some(5)); }
#[test]
fn text_range_can_extend_with_whitespace_after_a_multi_byte_character_earlier_in_the_line() {
let code = "a—b c"; let a = TextRange::new(0, 0, 0, 5); let b = TextRange::new(0, 8, 0, 9);
assert!(a.can_extend_with_whitespace(&b, &SourceText::new(code)));
}
}
#[cfg(test)]
mod position_tests {
use super::*;
#[test]
fn floor_char_boundary_clamps_to_the_line_and_rounds_down_inside_a_character() {
assert_eq!(floor_char_boundary("aæ¼¢b", 2), 1);
assert_eq!(floor_char_boundary("aæ¼¢b", 4), 4);
assert_eq!(floor_char_boundary("ab", 99), 2);
}
#[test]
fn paint_row_len_stops_before_trailing_whitespace() {
assert_eq!(paint_row_len("let x; \t"), SourceColumn::from_raw(6));
assert_eq!(paint_row_len(" "), SourceColumn::from_raw(0));
}
#[test]
fn row_len_is_bytes_not_characters() {
assert_eq!(row_len_of("abc"), SourceColumn::from_raw(3));
assert_eq!(row_len_of("é]"), SourceColumn::from_raw(3));
assert_eq!(row_len_of(""), SourceColumn::from_raw(0));
}
#[test]
fn screen_column_counts_cells_not_bytes_or_characters() {
assert_eq!(
screen_column_in("let a", SourceColumn::from_raw(5)),
ScreenColumn::from_raw(5)
);
assert_eq!(
screen_column_in("aéb", SourceColumn::from_raw(3)),
ScreenColumn::from_raw(2)
);
assert_eq!(
screen_column_in("aæ¼¢b", SourceColumn::from_raw(4)),
ScreenColumn::from_raw(3)
);
assert_eq!(
screen_column_in("e\u{0301}x", SourceColumn::from_raw(3)),
ScreenColumn::from_raw(1)
);
}
#[test]
fn screen_column_rounds_down_inside_a_multi_byte_character() {
assert_eq!(
screen_column_in("aæ¼¢", SourceColumn::from_raw(2)),
ScreenColumn::from_raw(1)
);
assert_eq!(
screen_column_in("aæ¼¢", SourceColumn::from_raw(4)),
ScreenColumn::from_raw(3)
);
}
#[test]
fn screen_column_is_monotonic_across_every_byte_column_of_a_mixed_row() {
let line = "a漢é\u{0301}b—c";
let mut previous = ScreenColumn::from_raw(0);
for column in 0..=line.len() {
let cells = screen_column_in(line, SourceColumn::from_raw(column));
assert!(
cells >= previous,
"screen column went backwards at byte {column} of {line:?}"
);
previous = cells;
}
assert_eq!(previous, ScreenColumn::from_raw(7));
}
}
#[cfg(test)]
mod corpus_position_invariants {
#[test]
fn corpus_ranges_are_addressable_byte_columns_sharing_nothing_but_line_terminators() {
let cases = crate::test::helper::handmade_test_case_dirs().expect("corpus");
let checked = std::sync::atomic::AtomicUsize::new(0);
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
let chunk = cases.len().div_ceil(threads).max(1);
std::thread::scope(|scope| {
for slice in cases.chunks(chunk) {
scope.spawn(|| check_cases(slice, &checked));
}
});
assert!(
checked.load(std::sync::atomic::Ordering::Relaxed) > 0,
"the corpus should have produced ranges to check"
);
}
fn check_cases(
cases: &[(String, std::path::PathBuf)],
checked: &std::sync::atomic::AtomicUsize,
) {
use crate::diff::text_range::{SourceColumn, SourceOffset, SourceRow, SourceText};
for (name, dir) in cases {
let Some((before, after)) =
crate::test::helper::code_pair_from_dir(dir).expect("fixture loads")
else {
continue;
};
let diff = crate::diff::diff_code(&before, &after);
let Some(ast) = diff.ast.as_ref() else {
continue;
};
let cache = crate::diff::NodeCache::build(&before, &after);
let text_diff = crate::diff::text::TextDiff::from(&before, &after, ast, &cache);
for (side, contents) in [(0usize, &before.contents), (1usize, &after.contents)] {
let rows: Vec<&str> = contents.split('\n').collect();
let text = SourceText::new(contents);
let mut spans: Vec<(usize, usize)> = Vec::new();
for range in text_diff.all(side) {
for (label, row, column) in [
("start", range.source.start_row, range.source.start_column),
("end", range.source.end_row, range.source.end_column),
] {
let Some(text) = rows.get(row) else {
assert_eq!(
column, 0,
"{name} side{side}: {label} past the last row must be column 0"
);
continue;
};
assert!(
column <= text.len(),
"{name} side{side}: {label} column {column} exceeds row {row}'s \
{} bytes - a character count cannot stand in for a byte column",
text.len()
);
assert!(
text.is_char_boundary(column),
"{name} side{side}: {label} column {column} falls inside a multi-byte \
character on row {row}"
);
checked.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
let offset = |row: usize, column: usize| -> usize {
text.byte_index(SourceRow::from_raw(row), SourceColumn::from_raw(column))
.map(SourceOffset::get)
.unwrap_or_else(|| {
panic!(
"{name} side{side}: r{row}c{column} is not addressable as a \
byte offset"
)
})
};
let start = offset(range.source.start_row, range.source.start_column);
let end = offset(range.source.end_row, range.source.end_column);
assert!(
end >= start,
"{name} side{side}: range ends before it starts ({start}..{end})"
);
if end > start {
spans.push((start, end));
}
}
for i in 0..spans.len() {
for j in (i + 1)..spans.len() {
let lo = spans[i].0.max(spans[j].0);
let hi = spans[i].1.min(spans[j].1);
if lo >= hi {
continue;
}
assert_eq!(
&contents[lo..hi],
"\n",
"{name} side{side}: ranges {:?} and {:?} both claim {:?}, which is not \
a line terminator",
spans[i],
spans[j],
&contents[lo..hi]
);
}
}
}
}
}
}