pub fn byte_offset(text: &str, line: u32, character: u32) -> usize {
let mut offset = 0;
for (number, line_text) in text.split_inclusive('\n').enumerate() {
if number as u32 == line {
return offset + column_offset(line_text, character);
}
offset += line_text.len();
}
text.len()
}
fn column_offset(line: &str, character: u32) -> usize {
let line = line.strip_suffix('\n').unwrap_or(line);
let line = line.strip_suffix('\r').unwrap_or(line);
let mut counted = 0;
for (offset, character_here) in line.char_indices() {
if counted >= character {
return offset;
}
let after = counted + character_here.len_utf16() as u32;
if after > character {
return offset;
}
counted = after;
}
line.len()
}
#[cfg(test)]
mod tests {
use super::*;
const TEXT: &str = "fn main() {\n let x = 1;\n}\n";
#[test]
fn a_position_is_a_line_and_a_column() {
assert_eq!(byte_offset(TEXT, 0, 0), 0);
assert_eq!(byte_offset(TEXT, 0, 3), 3);
assert_eq!(byte_offset(TEXT, 1, 4), "fn main() {\n ".len());
assert_eq!(byte_offset(TEXT, 2, 0), TEXT.len() - 2);
}
#[test]
fn columns_are_counted_in_utf16_code_units() {
let text = "let é = \"🦀\";";
assert_eq!(byte_offset(text, 0, 4), "let ".len());
assert_eq!(byte_offset(text, 0, 5), "let é".len());
assert_eq!(byte_offset(text, 0, 9), "let é = \"".len());
assert_eq!(byte_offset(text, 0, 11), "let é = \"🦀".len());
}
#[test]
fn a_column_inside_a_character_stops_before_it() {
let text = "🦀🦀";
assert_eq!(byte_offset(text, 0, 1), 0);
assert_eq!(byte_offset(text, 0, 2), "🦀".len());
assert_eq!(byte_offset(text, 0, 3), "🦀".len());
}
#[test]
fn a_position_past_the_end_is_the_end() {
assert_eq!(byte_offset(TEXT, 0, 999), "fn main() {".len());
assert_eq!(byte_offset(TEXT, 99, 0), TEXT.len());
assert_eq!(byte_offset("", 0, 0), 0);
}
#[test]
fn line_endings_are_not_part_of_the_line() {
let text = "one\r\ntwo\r\n";
assert_eq!(byte_offset(text, 0, 99), "one".len());
assert_eq!(byte_offset(text, 1, 0), "one\r\n".len());
assert_eq!(byte_offset(text, 1, 3), "one\r\ntwo".len());
}
}