use std::collections::HashMap;
use std::sync::Arc;
use lsp_types::Position;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Utf16Char {
byte_start: usize,
utf8_len: usize,
utf16_len: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LineIndex {
line_starts: Vec<usize>,
line_lengths: Vec<usize>,
len: usize,
utf16_lines: HashMap<u32, Vec<Utf16Char>>,
has_eof_line: bool,
}
impl LineIndex {
pub(crate) fn new(text: &str) -> LineIndex {
let bytes = text.as_bytes();
let len = text.len();
let mut line_starts = vec![0usize];
let mut line_lengths: Vec<usize> = Vec::new();
let mut utf16_lines: HashMap<u32, Vec<Utf16Char>> = HashMap::new();
let mut line_start = 0usize;
let mut cur_line: u32 = 0;
for (i, ch) in text.char_indices() {
if ch == '\n' {
let mut vis = i - line_start;
if vis > 0 && bytes[i - 1] == b'\r' {
vis -= 1;
}
line_lengths.push(vis);
line_starts.push(i + 1);
line_start = i + 1;
cur_line += 1;
} else if !ch.is_ascii() {
utf16_lines.entry(cur_line).or_default().push(Utf16Char {
byte_start: i - line_start,
utf8_len: ch.len_utf8(),
utf16_len: ch.len_utf16(),
});
}
}
line_lengths.push(len - line_start);
let has_eof_line = !text.is_empty() && bytes[len - 1] != b'\n';
LineIndex {
line_starts,
line_lengths,
len,
utf16_lines,
has_eof_line,
}
}
pub(crate) fn len(&self) -> usize {
self.len
}
pub(crate) fn offset_to_position(&self, offset: usize) -> Position {
let offset = offset.min(self.len);
let line = match self.line_starts.binary_search(&offset) {
Ok(i) => i,
Err(i) => i - 1,
};
let byte_col = (offset - self.line_starts[line]).min(self.line_lengths[line]);
Position {
line: line as u32,
character: self.utf16_column(line as u32, byte_col) as u32,
}
}
pub(crate) fn position_to_offset(&self, position: Position) -> Option<usize> {
let line = position.line as usize;
let (line_start, vis) = if line < self.line_starts.len() {
(self.line_starts[line], self.line_lengths[line])
} else if self.has_eof_line && line == self.line_starts.len() {
(self.len, 0)
} else {
return None;
};
let byte_col = self.utf16_to_byte(line as u32, position.character as usize, vis);
Some(line_start + byte_col)
}
fn utf16_column(&self, line: u32, byte_col: usize) -> usize {
match self.utf16_lines.get(&line) {
None => byte_col,
Some(wides) => {
let mut ascii = byte_col;
let mut utf16 = 0usize;
for c in wides {
if c.byte_start >= byte_col {
break;
}
utf16 += c.utf16_len;
let consumed = (c.byte_start + c.utf8_len).min(byte_col) - c.byte_start;
ascii -= consumed;
}
ascii + utf16
}
}
}
fn utf16_to_byte(&self, line: u32, character: usize, vis: usize) -> usize {
match self.utf16_lines.get(&line) {
None => character.min(vis),
Some(wides) => {
let mut u16_col = 0usize;
let mut byte = 0usize;
let mut wi = 0usize;
while byte < vis {
if u16_col >= character {
return byte;
}
if wi < wides.len() && wides[wi].byte_start == byte {
u16_col += wides[wi].utf16_len;
byte += wides[wi].utf8_len;
wi += 1;
} else {
u16_col += 1;
byte += 1;
}
}
vis
}
}
}
}
#[salsa::tracked(lru = 512)]
pub(crate) fn line_index(
db: &dyn crate::salsa::Db,
file: crate::salsa::FileText,
) -> Arc<LineIndex> {
Arc::new(LineIndex::new(file.content_or_empty(db)))
}
#[cfg(test)]
mod tests {
use super::*;
fn pos(line: u32, character: u32) -> Position {
Position { line, character }
}
#[test]
fn offset_to_position_simple() {
let idx = LineIndex::new("hello\nworld\n");
assert_eq!(idx.offset_to_position(0), pos(0, 0));
assert_eq!(idx.offset_to_position(3), pos(0, 3));
assert_eq!(idx.offset_to_position(6), pos(1, 0));
assert_eq!(idx.offset_to_position(9), pos(1, 3));
}
#[test]
fn offset_to_position_utf16() {
let idx = LineIndex::new("café\n");
assert_eq!(idx.offset_to_position(0).character, 0);
assert_eq!(idx.offset_to_position(3).character, 3);
assert_eq!(idx.offset_to_position(5).character, 4);
}
#[test]
fn offset_to_position_emoji() {
let idx = LineIndex::new("hi👋\n");
assert_eq!(idx.offset_to_position(2).character, 2);
assert_eq!(idx.offset_to_position(6).character, 4);
}
#[test]
fn offset_to_position_crlf() {
let idx = LineIndex::new("hello\r\nworld\r\n");
assert_eq!(idx.offset_to_position(0), pos(0, 0));
assert_eq!(idx.offset_to_position(3), pos(0, 3));
assert_eq!(idx.offset_to_position(7), pos(1, 0));
assert_eq!(idx.offset_to_position(10), pos(1, 3));
}
#[test]
fn offset_to_position_inside_multibyte_char() {
let idx = LineIndex::new("ä\n");
assert_eq!(idx.offset_to_position(1), pos(0, 1));
}
#[test]
fn offset_to_position_inside_multibyte_char_crlf() {
let idx = LineIndex::new("åäö\r\nnext\r\n");
assert_eq!(idx.offset_to_position(1), pos(0, 1));
assert_eq!(idx.offset_to_position(5), pos(0, 3));
assert_eq!(idx.offset_to_position(8), pos(1, 0));
}
#[test]
fn position_to_offset_simple() {
let idx = LineIndex::new("hello\nworld\n");
assert_eq!(idx.position_to_offset(pos(0, 0)), Some(0));
assert_eq!(idx.position_to_offset(pos(0, 3)), Some(3));
assert_eq!(idx.position_to_offset(pos(0, 5)), Some(5));
assert_eq!(idx.position_to_offset(pos(1, 0)), Some(6));
assert_eq!(idx.position_to_offset(pos(1, 3)), Some(9));
}
#[test]
fn position_to_offset_utf8() {
let idx = LineIndex::new("café\nworld\n");
assert_eq!(idx.position_to_offset(pos(0, 0)), Some(0));
assert_eq!(idx.position_to_offset(pos(0, 1)), Some(1));
assert_eq!(idx.position_to_offset(pos(0, 2)), Some(2));
assert_eq!(idx.position_to_offset(pos(0, 3)), Some(3));
assert_eq!(idx.position_to_offset(pos(0, 4)), Some(5));
}
#[test]
fn position_to_offset_emoji() {
let idx = LineIndex::new("hi👋\n");
assert_eq!(idx.position_to_offset(pos(0, 2)), Some(2));
assert_eq!(idx.position_to_offset(pos(0, 4)), Some(6));
}
#[test]
fn position_to_offset_crlf() {
let idx = LineIndex::new("hello\r\nworld\r\n");
assert_eq!(idx.position_to_offset(pos(0, 0)), Some(0));
assert_eq!(idx.position_to_offset(pos(0, 3)), Some(3));
assert_eq!(idx.position_to_offset(pos(1, 0)), Some(7));
assert_eq!(idx.position_to_offset(pos(1, 3)), Some(10));
}
#[test]
fn position_to_offset_trailing_lines() {
let idx = LineIndex::new("hello\nworld\n");
assert_eq!(idx.position_to_offset(pos(2, 0)), Some(12));
assert_eq!(idx.position_to_offset(pos(3, 0)), None);
let idx = LineIndex::new("hello\nworld");
assert_eq!(idx.position_to_offset(pos(2, 0)), Some(11));
assert_eq!(idx.position_to_offset(pos(2, 5)), Some(11));
assert_eq!(idx.position_to_offset(pos(3, 0)), None);
}
#[test]
fn empty_document() {
let idx = LineIndex::new("");
assert_eq!(idx.offset_to_position(0), pos(0, 0));
assert_eq!(idx.position_to_offset(pos(0, 0)), Some(0));
assert_eq!(idx.position_to_offset(pos(1, 0)), None);
}
#[test]
fn offset_past_end_clamps() {
let idx = LineIndex::new("hi");
assert_eq!(idx.offset_to_position(999), pos(0, 2));
}
#[test]
fn position_column_past_line_clamps() {
let idx = LineIndex::new("hi\nthere\n");
assert_eq!(idx.position_to_offset(pos(0, 99)), Some(2));
}
}