use std::collections::HashMap;
use std::ops::Range;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use lsp_types::Position;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LineIndex {
text: Arc<str>,
line_starts: Vec<usize>,
wide_lines: Vec<bool>,
}
impl LineIndex {
pub(crate) fn new(text: &str) -> LineIndex {
LineIndex::from_arc(Arc::from(text))
}
pub(crate) fn from_arc(text: Arc<str>) -> LineIndex {
let mut line_starts = Vec::with_capacity(text.len() / 40 + 1);
line_starts.push(0usize);
line_starts.extend(memchr::memchr_iter(b'\n', text.as_bytes()).map(|at| at + 1));
let mut index = LineIndex {
text,
wide_lines: vec![false; line_starts.len()],
line_starts,
};
for line in 0..index.line_starts.len() {
index.wide_lines[line] = index.recompute_wide(line);
}
index
}
pub(crate) fn text_arc(&self) -> Arc<str> {
Arc::clone(&self.text)
}
pub(crate) fn indexes(&self, text: &Arc<str>) -> bool {
Arc::ptr_eq(&self.text, text)
}
pub(crate) fn len(&self) -> usize {
self.text.len()
}
fn line_span(&self, line: usize) -> Range<usize> {
let start = self.line_starts[line];
let end = self
.line_starts
.get(line + 1)
.copied()
.unwrap_or(self.text.len());
start..end
}
fn recompute_wide(&self, line: usize) -> bool {
!self.text.as_bytes()[self.line_span(line)].is_ascii()
}
fn line_len(&self, line: usize) -> usize {
let Range { start, end } = self.line_span(line);
if line + 1 == self.line_starts.len() {
return end - start;
}
let mut vis = end - 1 - start;
if vis > 0 && self.text.as_bytes()[end - 2] == b'\r' {
vis -= 1;
}
vis
}
fn has_eof_line(&self) -> bool {
!self.text.is_empty() && !self.text.as_bytes().ends_with(b"\n")
}
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_len(line));
Position {
line: line as u32,
character: self.utf16_column(line, 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_len(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, position.character as usize, vis);
Some(line_start + byte_col)
}
fn utf16_column(&self, line: usize, byte_col: usize) -> usize {
if !self.wide_lines[line] {
return byte_col;
}
let start = self.line_starts[line];
let mut utf16 = 0usize;
let mut byte = 0usize;
for ch in self.text[start..].chars() {
if byte >= byte_col {
break;
}
utf16 += ch.len_utf16();
byte += ch.len_utf8();
}
utf16
}
fn utf16_to_byte(&self, line: usize, character: usize, vis: usize) -> usize {
if !self.wide_lines.get(line).copied().unwrap_or(false) {
return character.min(vis);
}
let start = self.line_starts[line];
let mut chars = self.text[start..start + vis].chars();
let mut u16_col = 0usize;
let mut byte = 0usize;
while byte < vis {
if u16_col >= character {
return byte;
}
let ch = chars.next().expect("visible line has a char at `byte`");
u16_col += ch.len_utf16();
byte += ch.len_utf8();
}
vis
}
pub(crate) fn replace_range(&mut self, range: Range<usize>, insert: &str) {
let Range { start, end } = range;
assert!(start <= end, "reversed edit range {start}..{end}");
let first = self.line_starts.partition_point(|&at| at <= start);
let last = self.line_starts.partition_point(|&at| at <= end);
let delta = insert.len() as isize - (end - start) as isize;
if delta != 0 {
for at in &mut self.line_starts[last..] {
*at = at.wrapping_add_signed(delta);
}
}
let inserted: Vec<usize> = memchr::memchr_iter(b'\n', insert.as_bytes())
.map(|at| start + at + 1)
.collect();
let inserted_count = inserted.len();
self.line_starts.splice(first..last, inserted);
let old = &self.text;
let mut text = String::with_capacity(old.len() - (end - start) + insert.len());
text.push_str(&old[..start]);
text.push_str(insert);
text.push_str(&old[end..]);
self.text = Arc::from(text);
self.wide_lines
.splice(first..last, std::iter::repeat_n(false, inserted_count));
for line in (first - 1)..(first + inserted_count) {
self.wide_lines[line] = self.recompute_wide(line);
}
self.debug_assert_in_step();
}
fn debug_assert_in_step(&self) {
debug_assert!(
*self == LineIndex::from_arc(Arc::clone(&self.text)),
"line index drifted from the text it indexes"
);
}
}
const MAX_LINE_INDEXES: usize = 256;
#[derive(Default)]
pub(crate) struct LineIndexCache {
files: HashMap<crate::salsa::FileText, Entry>,
clock: u64,
write_rebuilds: u64,
read_rebuilds: u64,
}
struct Entry {
index: Arc<LineIndex>,
used: u64,
}
impl LineIndexCache {
fn touch(&mut self, file: crate::salsa::FileText) -> Option<&mut Entry> {
self.clock += 1;
let clock = self.clock;
let entry = self.files.get_mut(&file)?;
entry.used = clock;
Some(entry)
}
fn get(&mut self, file: crate::salsa::FileText, text: &Arc<str>) -> Option<Arc<LineIndex>> {
match self.touch(file) {
Some(entry) if entry.index.indexes(text) => Some(Arc::clone(&entry.index)),
Some(_) => {
self.files.remove(&file);
None
}
None => None,
}
}
fn take(&mut self, file: crate::salsa::FileText, text: &Arc<str>) -> Option<Arc<LineIndex>> {
self.files
.remove(&file)
.filter(|entry| entry.index.indexes(text))
.map(|entry| entry.index)
}
fn insert(&mut self, file: crate::salsa::FileText, index: Arc<LineIndex>) {
self.clock += 1;
let used = self.clock;
self.files.insert(file, Entry { index, used });
self.evict_over_budget();
}
fn remove(&mut self, file: crate::salsa::FileText) {
self.files.remove(&file);
}
fn len(&self) -> usize {
self.files.len()
}
fn evict_over_budget(&mut self) {
if self.files.len() <= MAX_LINE_INDEXES {
return;
}
let over = self.files.len() - MAX_LINE_INDEXES;
let mut stamps: Vec<u64> = self.files.values().map(|entry| entry.used).collect();
stamps.select_nth_unstable(over - 1);
let threshold = stamps[over - 1];
self.files.retain(|_, entry| entry.used > threshold);
}
}
pub(crate) type SharedLineIndexCache = Arc<Mutex<LineIndexCache>>;
fn lock(cache: &Mutex<LineIndexCache>) -> MutexGuard<'_, LineIndexCache> {
cache.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) fn line_index(
cache: &Mutex<LineIndexCache>,
db: &dyn crate::salsa::Db,
file: crate::salsa::FileText,
) -> Arc<LineIndex> {
let Some(text) = file.text(db).clone() else {
return Arc::new(LineIndex::from_arc(Arc::from("")));
};
if let Some(hit) = lock(cache).get(file, &text) {
return hit;
}
let built = Arc::new(LineIndex::from_arc(Arc::clone(&text)));
let mut guard = lock(cache);
guard.read_rebuilds += 1;
guard.insert(file, Arc::clone(&built));
built
}
pub(crate) fn take_for_write(
cache: &Mutex<LineIndexCache>,
file: crate::salsa::FileText,
text: &Arc<str>,
) -> Arc<LineIndex> {
let mut guard = lock(cache);
if let Some(taken) = guard.take(file, text) {
return taken;
}
guard.write_rebuilds += 1;
drop(guard);
Arc::new(LineIndex::from_arc(Arc::clone(text)))
}
pub(crate) fn store_from_write(
cache: &Mutex<LineIndexCache>,
file: crate::salsa::FileText,
current: Option<&Arc<str>>,
index: Arc<LineIndex>,
) {
let mut guard = lock(cache);
match current {
Some(current) if index.indexes(current) => guard.insert(file, index),
_ => guard.remove(file),
}
}
pub(crate) fn retire(cache: &Mutex<LineIndexCache>, file: crate::salsa::FileText) {
lock(cache).remove(file);
}
pub(crate) fn cached_count(cache: &Mutex<LineIndexCache>) -> usize {
lock(cache).len()
}
pub(crate) fn write_rebuilds(cache: &Mutex<LineIndexCache>) -> u64 {
lock(cache).write_rebuilds
}
pub(crate) fn read_rebuilds(cache: &Mutex<LineIndexCache>) -> u64 {
lock(cache).read_rebuilds
}
#[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));
}
#[test]
fn patching_matches_a_rescan() {
let texts = [
"",
"\n",
"\n\n",
"abc",
"ab\ncd\nef\n",
"a\r\nb\r\n",
"\u{1F600}\nx\n",
"café\r\nx",
"a\rb\n",
"ä",
];
let inserts = [
"",
"z",
"\n",
"\n\n",
"x\ny\n",
"\r\n",
"\u{1F600}",
"é",
"\r",
];
for text in texts {
for start in 0..=text.len() {
if !text.is_char_boundary(start) {
continue;
}
for end in start..=text.len() {
if !text.is_char_boundary(end) {
continue;
}
for insert in inserts {
let mut patched = LineIndex::new(text);
patched.replace_range(start..end, insert);
let mut edited = text.to_string();
edited.replace_range(start..end, insert);
assert_eq!(
patched,
LineIndex::new(&edited),
"patching {text:?}[{start}..{end}] with {insert:?} \
diverged from a rescan"
);
}
}
}
}
}
#[test]
fn an_index_only_claims_the_allocation_it_was_built_for() {
let text: Arc<str> = Arc::from("ab\ncd\n");
let mut index = LineIndex::from_arc(Arc::clone(&text));
assert!(index.indexes(&text));
let twin: Arc<str> = Arc::from("ab\ncd\n");
assert_eq!(&*twin, &*text);
assert!(!index.indexes(&twin));
index.replace_range(2..2, "x");
assert!(!index.indexes(&text));
assert!(index.indexes(&index.text_arc()));
}
mod cache {
use super::super::*;
use crate::salsa::{FileText, SalsaDb};
fn file(db: &SalsaDb, text: &str) -> (FileText, Arc<str>) {
let file = FileText::from_str(db, text);
let held = file.text(db).clone().expect("just set");
(file, held)
}
fn cache() -> SharedLineIndexCache {
SharedLineIndexCache::default()
}
#[test]
fn a_reader_builds_once_and_then_hits() {
let db = SalsaDb::default();
let (file, _held) = file(&db, "ab\ncd\n");
let cache = cache();
let first = line_index(&cache, &db, file);
let second = line_index(&cache, &db, file);
assert!(Arc::ptr_eq(&first, &second), "the second read must hit");
assert_eq!(read_rebuilds(&cache), 1);
assert_eq!(cached_count(&cache), 1);
}
#[test]
fn an_equal_but_distinct_allocation_misses_and_evicts() {
let db = SalsaDb::default();
let (file, held) = file(&db, "ab\ncd\n");
let cache = cache();
lock(&cache).insert(file, Arc::new(LineIndex::from_arc(Arc::clone(&held))));
let twin: Arc<str> = Arc::from("ab\ncd\n");
assert_eq!(&*twin, &*held);
assert!(lock(&cache).get(file, &twin).is_none());
assert_eq!(
cached_count(&cache),
0,
"a stale entry must be dropped, not kept"
);
}
#[test]
fn a_take_removes_the_entry_and_yields_a_unique_handle() {
let db = SalsaDb::default();
let (file, held) = file(&db, "ab\ncd\n");
let cache = cache();
store_from_write(
&cache,
file,
Some(&held),
Arc::new(LineIndex::from_arc(Arc::clone(&held))),
);
let mut taken = take_for_write(&cache, file, &held);
assert_eq!(cached_count(&cache), 0);
assert_eq!(
write_rebuilds(&cache),
0,
"a validating entry is not a rebuild"
);
assert_eq!(
Arc::strong_count(&taken),
1,
"the write phase must hold the only reference, or `make_mut` copies"
);
let before = Arc::as_ptr(&taken);
Arc::make_mut(&mut taken).replace_range(2..2, "x");
assert_eq!(Arc::as_ptr(&taken), before);
}
#[test]
fn a_take_that_misses_counts_a_rebuild_and_caches_nothing() {
let db = SalsaDb::default();
let (file, held) = file(&db, "ab\ncd\n");
let cache = cache();
let taken = take_for_write(&cache, file, &held);
assert!(taken.indexes(&held));
assert_eq!(write_rebuilds(&cache), 1);
assert_eq!(cached_count(&cache), 0);
}
#[test]
fn a_store_whose_text_salsa_does_not_hold_caches_nothing() {
let db = SalsaDb::default();
let (file, held) = file(&db, "ab\ncd\n");
let cache = cache();
let orphan: Arc<str> = Arc::from("ab\ncd\n");
store_from_write(
&cache,
file,
Some(&held),
Arc::new(LineIndex::from_arc(orphan)),
);
assert_eq!(cached_count(&cache), 0);
}
#[test]
fn retiring_forgets_the_document() {
let db = SalsaDb::default();
let (file, held) = file(&db, "ab\ncd\n");
let cache = cache();
store_from_write(
&cache,
file,
Some(&held),
Arc::new(LineIndex::from_arc(Arc::clone(&held))),
);
assert_eq!(cached_count(&cache), 1);
retire(&cache, file);
assert_eq!(cached_count(&cache), 0);
}
#[test]
fn eviction_keeps_the_budget_and_spares_the_most_recently_used() {
let db = SalsaDb::default();
let cache = cache();
let files: Vec<(FileText, Arc<str>)> = (0..MAX_LINE_INDEXES + 8)
.map(|index| file(&db, &format!("# {index}\n")))
.collect();
for (file, held) in &files {
store_from_write(
&cache,
*file,
Some(held),
Arc::new(LineIndex::from_arc(Arc::clone(held))),
);
}
assert_eq!(cached_count(&cache), MAX_LINE_INDEXES);
let (hot, hot_text) = files.last().unwrap();
assert!(
lock(&cache).get(*hot, hot_text).is_some(),
"the most recently stored document must survive"
);
}
#[test]
fn an_unloaded_file_is_answered_without_being_cached() {
let db = SalsaDb::default();
let file = FileText::new(&db, None);
let cache = cache();
let index = line_index(&cache, &db, file);
assert_eq!(index.len(), 0);
assert_eq!(cached_count(&cache), 0);
assert_eq!(read_rebuilds(&cache), 0);
}
}
#[test]
fn an_edit_leaves_earlier_text_handles_alone() {
let mut index = LineIndex::new("ab\ncd");
let before = index.text_arc();
assert!(Arc::ptr_eq(&before, &index.text_arc()));
index.replace_range(2..2, "\nxy");
assert!(
!Arc::ptr_eq(&before, &index.text_arc()),
"an edit must not mutate a shared allocation"
);
assert_eq!(&*before, "ab\ncd");
assert_eq!(&*index.text_arc(), "ab\nxy\ncd");
}
}