use std::collections::HashMap;
use lsp_types::{Position, PositionEncodingKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PositionEncoding {
Utf8,
#[default]
Utf16,
}
impl PositionEncoding {
pub fn to_kind(self) -> PositionEncodingKind {
match self {
PositionEncoding::Utf8 => PositionEncodingKind::UTF8,
PositionEncoding::Utf16 => PositionEncodingKind::UTF16,
}
}
fn metric(self) -> Metric {
match self {
PositionEncoding::Utf8 => Metric::Utf8,
PositionEncoding::Utf16 => Metric::Utf16,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LineCol {
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct WideChar {
start: u32,
end: u32,
}
impl WideChar {
fn len(self) -> u32 {
self.end - self.start
}
fn len_utf16(self) -> u32 {
if self.len() == 4 { 2 } else { 1 }
}
}
#[derive(Clone, Copy)]
enum Metric {
Utf8,
Utf16,
CodePoint,
}
impl Metric {
fn wide_units(self, w: WideChar) -> u32 {
match self {
Metric::Utf8 => w.len(),
Metric::Utf16 => w.len_utf16(),
Metric::CodePoint => 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LineIndex {
line_starts: Vec<usize>,
line_wide_chars: HashMap<usize, Vec<WideChar>>,
len: usize,
}
impl LineIndex {
pub fn new(text: &str) -> Self {
let mut line_starts = Vec::with_capacity(text.len() / 40 + 1);
line_starts.push(0);
let mut line_wide_chars: HashMap<usize, Vec<WideChar>> = HashMap::new();
let mut cur_line = 0usize;
let mut line_start = 0usize;
for (offset, ch) in text.char_indices() {
if ch == '\n' {
line_starts.push(offset + 1);
cur_line += 1;
line_start = offset + 1;
continue;
}
let bytes = ch.len_utf8();
if bytes > 1 {
line_wide_chars.entry(cur_line).or_default().push(WideChar {
start: (offset - line_start) as u32,
end: (offset + bytes - line_start) as u32,
});
}
}
Self {
line_starts,
line_wide_chars,
len: text.len(),
}
}
pub fn byte_to_lc(&self, offset: usize) -> LineCol {
let clamped = offset.min(self.len);
let line = self.line_index_for(clamped);
let rel = clamped - self.line_starts[line];
LineCol {
line: line + 1,
column: self.col_in(line, rel, Metric::CodePoint) as usize + 1,
}
}
pub fn byte_to_position(&self, offset: usize, encoding: PositionEncoding) -> Position {
let clamped = offset.min(self.len);
let line = self.line_index_for(clamped);
let rel = clamped - self.line_starts[line];
let character = self.col_in(line, rel, encoding.metric());
Position::new(line as u32, character)
}
pub fn position_to_byte(&self, position: Position, encoding: PositionEncoding) -> usize {
let line = position.line as usize;
if line >= self.line_starts.len() {
return self.len;
}
self.byte_at_col(line, position.character, encoding.metric())
}
pub fn byte_to_line(&self, offset: usize) -> u32 {
self.line_index_for(offset.min(self.len)) as u32
}
pub fn line_count(&self) -> usize {
self.line_starts.len()
}
pub fn line_start(&self, line: usize) -> usize {
self.line_starts.get(line).copied().unwrap_or(self.len)
}
fn col_in(&self, line: usize, rel: usize, metric: Metric) -> u32 {
let mut shortfall = 0u32;
if let Some(wides) = self.line_wide_chars.get(&line) {
for w in wides {
if w.end as usize <= rel {
shortfall += w.len() - metric.wide_units(*w);
} else {
break;
}
}
}
rel as u32 - shortfall
}
fn byte_at_col(&self, line: usize, target_col: u32, metric: Metric) -> usize {
let line_start = self.line_starts[line];
let line_end = self.line_starts.get(line + 1).copied().unwrap_or(self.len);
let mut col = 0u32;
let mut byte = line_start;
if let Some(wides) = self.line_wide_chars.get(&line) {
for w in wides {
let w_start = line_start + w.start as usize;
let ascii = (w_start - byte) as u32;
if col + ascii >= target_col {
return byte + (target_col - col) as usize;
}
col += ascii;
byte = w_start;
let units = metric.wide_units(*w);
if col + units > target_col {
return byte;
}
col += units;
byte = line_start + w.end as usize;
}
}
(byte + (target_col - col) as usize).min(line_end)
}
fn line_index_for(&self, offset: usize) -> usize {
match self.line_starts.binary_search(&offset) {
Ok(idx) => idx,
Err(idx) => idx.saturating_sub(1),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use PositionEncoding::{Utf8, Utf16};
#[test]
fn empty_string() {
let idx = LineIndex::new("");
assert_eq!(idx.byte_to_lc(0), LineCol { line: 1, column: 1 });
assert_eq!(idx.byte_to_position(0, Utf16), Position::new(0, 0));
assert_eq!(idx.byte_to_position(0, Utf8), Position::new(0, 0));
}
#[test]
fn single_line() {
let idx = LineIndex::new("abc");
assert_eq!(idx.byte_to_lc(0).column, 1);
assert_eq!(idx.byte_to_lc(2).column, 3);
assert_eq!(idx.byte_to_lc(3).column, 4);
assert_eq!(idx.byte_to_position(2, Utf16), Position::new(0, 2));
assert_eq!(idx.byte_to_position(2, Utf8), Position::new(0, 2));
}
#[test]
fn multi_line() {
let idx = LineIndex::new("ab\ncd\nef");
assert_eq!(idx.byte_to_lc(0), LineCol { line: 1, column: 1 });
assert_eq!(idx.byte_to_lc(2), LineCol { line: 1, column: 3 }); assert_eq!(idx.byte_to_lc(3), LineCol { line: 2, column: 1 });
assert_eq!(idx.byte_to_lc(6), LineCol { line: 3, column: 1 });
assert_eq!(idx.byte_to_position(6, Utf16), Position::new(2, 0));
assert_eq!(idx.byte_to_position(6, Utf8), Position::new(2, 0));
}
#[test]
fn utf8_multibyte() {
let idx = LineIndex::new("\u{00e1}b\nc");
assert_eq!(idx.byte_to_lc(2), LineCol { line: 1, column: 2 });
assert_eq!(idx.byte_to_position(2, Utf16), Position::new(0, 1));
assert_eq!(idx.byte_to_position(2, Utf8), Position::new(0, 2));
assert_eq!(idx.byte_to_lc(3), LineCol { line: 1, column: 3 });
assert_eq!(idx.byte_to_position(3, Utf16), Position::new(0, 2));
assert_eq!(idx.byte_to_position(3, Utf8), Position::new(0, 3));
}
#[test]
fn utf16_surrogate_pair() {
let idx = LineIndex::new("\u{1F600}x");
assert_eq!(idx.byte_to_lc(4), LineCol { line: 1, column: 2 });
assert_eq!(idx.byte_to_position(4, Utf16), Position::new(0, 2));
assert_eq!(idx.byte_to_position(4, Utf8), Position::new(0, 4));
assert_eq!(idx.byte_to_position(5, Utf16), Position::new(0, 3));
assert_eq!(idx.byte_to_position(5, Utf8), Position::new(0, 5));
}
#[test]
fn offset_past_end_clamps() {
let idx = LineIndex::new("abc");
assert_eq!(idx.byte_to_lc(100), LineCol { line: 1, column: 4 });
}
#[test]
fn trailing_newline() {
let idx = LineIndex::new("ab\n");
assert_eq!(idx.byte_to_lc(3), LineCol { line: 2, column: 1 });
}
#[test]
fn line_start_clamps_past_the_end() {
let idx = LineIndex::new("ab\ncd");
assert_eq!(idx.line_start(0), 0);
assert_eq!(idx.line_start(1), 3);
assert_eq!(idx.line_start(2), 5);
assert_eq!(idx.line_start(99), 5);
}
#[test]
fn position_to_byte_round_trips_both_encodings() {
let text = "ab\ncde\nf\u{00e1}g\n\u{1F600}h";
let idx = LineIndex::new(text);
for encoding in [Utf8, Utf16] {
for offset in 0..=text.len() {
if !text.is_char_boundary(offset) {
continue;
}
let pos = idx.byte_to_position(offset, encoding);
assert_eq!(
idx.position_to_byte(pos, encoding),
offset,
"offset {offset} encoding {encoding:?}"
);
}
}
}
#[test]
fn position_to_byte_handles_wide_chars_and_overshoot() {
let idx = LineIndex::new("\u{1F600}x\ny");
assert_eq!(idx.position_to_byte(Position::new(0, 0), Utf16), 0);
assert_eq!(idx.position_to_byte(Position::new(0, 2), Utf16), 4); assert_eq!(idx.position_to_byte(Position::new(1, 0), Utf16), 6); assert_eq!(idx.position_to_byte(Position::new(0, 1), Utf16), 0);
assert_eq!(idx.position_to_byte(Position::new(0, 99), Utf16), 6);
assert_eq!(idx.position_to_byte(Position::new(9, 0), Utf16), 7);
assert_eq!(idx.position_to_byte(Position::new(0, 4), Utf8), 4); assert_eq!(idx.position_to_byte(Position::new(0, 5), Utf8), 5); }
#[test]
fn to_kind_maps_to_lsp() {
assert_eq!(Utf8.to_kind(), PositionEncodingKind::UTF8);
assert_eq!(Utf16.to_kind(), PositionEncodingKind::UTF16);
}
}