pub(super) fn byte_offset_to_row_col(lines: &[String], byte_offset: usize) -> (usize, usize) {
let mut consumed = 0usize;
for (row, line) in lines.iter().enumerate() {
let line_byte_len = line.len();
if consumed + line_byte_len >= byte_offset {
let col = line[..byte_offset - consumed].chars().count();
return (row, col);
}
consumed += line_byte_len + 1;
}
let last_row = lines.len().saturating_sub(1);
let last_col = lines.last().map_or(0, |l| l.chars().count());
(last_row, last_col)
}
pub(super) fn row_col_to_byte_offset(lines: &[String], row: usize, char_col: usize) -> usize {
let mut offset = 0usize;
for (i, line) in lines.iter().enumerate() {
if i == row {
let byte_col = line
.char_indices()
.nth(char_col)
.map_or(line.len(), |(idx, _)| idx);
return offset + byte_col;
}
offset += line.len() + 1; }
offset
}
pub(super) fn char_col_to_byte_offset(line: &str, char_col: usize) -> usize {
line.char_indices()
.nth(char_col)
.map_or(line.len(), |(idx, _)| idx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_line_start() {
let lines = vec!["hello".to_string()];
assert_eq!(byte_offset_to_row_col(&lines, 0), (0, 0));
assert_eq!(row_col_to_byte_offset(&lines, 0, 0), 0);
}
#[test]
fn single_line_middle() {
let lines = vec!["hello".to_string()];
assert_eq!(byte_offset_to_row_col(&lines, 3), (0, 3));
assert_eq!(row_col_to_byte_offset(&lines, 0, 3), 3);
}
#[test]
fn single_line_end() {
let lines = vec!["hello".to_string()];
assert_eq!(byte_offset_to_row_col(&lines, 5), (0, 5));
assert_eq!(row_col_to_byte_offset(&lines, 0, 5), 5);
}
#[test]
fn multi_line_first_line() {
let lines = vec!["hello".to_string(), "world".to_string()];
assert_eq!(byte_offset_to_row_col(&lines, 3), (0, 3));
assert_eq!(row_col_to_byte_offset(&lines, 0, 3), 3);
}
#[test]
fn multi_line_second_line() {
let lines = vec!["hello".to_string(), "world".to_string()];
assert_eq!(byte_offset_to_row_col(&lines, 6), (1, 0));
assert_eq!(row_col_to_byte_offset(&lines, 1, 0), 6);
}
#[test]
fn multi_line_second_line_offset() {
let lines = vec!["hello".to_string(), "world".to_string()];
assert_eq!(byte_offset_to_row_col(&lines, 9), (1, 3));
assert_eq!(row_col_to_byte_offset(&lines, 1, 3), 9);
}
#[test]
fn utf8_multibyte() {
let lines = vec!["你好".to_string()];
assert_eq!(byte_offset_to_row_col(&lines, 0), (0, 0));
assert_eq!(byte_offset_to_row_col(&lines, 3), (0, 1));
assert_eq!(byte_offset_to_row_col(&lines, 6), (0, 2));
assert_eq!(row_col_to_byte_offset(&lines, 0, 0), 0);
assert_eq!(row_col_to_byte_offset(&lines, 0, 1), 3);
assert_eq!(row_col_to_byte_offset(&lines, 0, 2), 6);
}
#[test]
fn roundtrip_multiline() {
let lines = vec!["abc".to_string(), "def".to_string(), "ghi".to_string()];
for byte_offset in 0..=11 {
let (row, col) = byte_offset_to_row_col(&lines, byte_offset);
let back = row_col_to_byte_offset(&lines, row, col);
assert_eq!(back, byte_offset, "roundtrip failed at byte {byte_offset}");
}
}
#[test]
fn past_end_clamps() {
let lines = vec!["hello".to_string()];
let (row, col) = byte_offset_to_row_col(&lines, 100);
assert_eq!(row, 0);
assert_eq!(col, 5);
}
#[test]
fn empty_content() {
let lines = vec!["".to_string()];
assert_eq!(byte_offset_to_row_col(&lines, 0), (0, 0));
assert_eq!(row_col_to_byte_offset(&lines, 0, 0), 0);
}
}