use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use lsp_types::{Position, TextDocumentContentChangeEvent, TextDocumentItem, Uri};
use ropey::Rope;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PositionEncoding {
Utf8,
Utf16,
}
impl Default for PositionEncoding {
fn default() -> Self {
Self::Utf16
}
}
#[derive(Debug, Clone)]
pub struct Document {
uri: Uri,
language_id: String,
version: i32,
text: Rope,
}
impl Document {
pub fn uri(&self) -> &Uri {
&self.uri
}
pub fn language_id(&self) -> &str {
&self.language_id
}
pub fn version(&self) -> i32 {
self.version
}
pub fn text(&self) -> String {
self.text.to_string()
}
pub fn position_to_offset(
&self,
encoding: PositionEncoding,
position: Position,
) -> Option<usize> {
let line_idx = position.line as usize;
if line_idx >= self.text.len_lines() {
return None;
}
let line_start_byte = self.text.line_to_byte(line_idx);
let line_text: String = self.text.line(line_idx).into();
match encoding {
PositionEncoding::Utf8 => {
let byte_in_line = position.character as usize;
let content_len = line_text.trim_end_matches(['\r', '\n']).len();
if byte_in_line > content_len || !line_text.is_char_boundary(byte_in_line) {
return None;
}
Some(line_start_byte + byte_in_line)
}
PositionEncoding::Utf16 => {
let mut utf16_count = 0usize;
for (byte_idx, ch) in line_text.char_indices() {
if utf16_count == position.character as usize {
return Some(line_start_byte + byte_idx);
}
utf16_count += ch.len_utf16();
}
if utf16_count == position.character as usize {
return Some(line_start_byte + line_text.len());
}
None
}
}
}
pub fn offset_to_position(
&self,
encoding: PositionEncoding,
offset: usize,
) -> Option<Position> {
if offset > self.text.len_bytes() {
return None;
}
let line_idx = self.text.byte_to_line(offset);
let line_start_byte = self.text.line_to_byte(line_idx);
let line_offset = offset - line_start_byte;
let line_text: String = self.text.line(line_idx).into();
match encoding {
PositionEncoding::Utf8 => Some(Position {
line: line_idx as u32,
character: line_offset as u32,
}),
PositionEncoding::Utf16 => {
let mut utf16_count = 0usize;
for (byte_idx, ch) in line_text.char_indices() {
if byte_idx == line_offset {
return Some(Position {
line: line_idx as u32,
character: utf16_count as u32,
});
}
utf16_count += ch.len_utf16();
}
Some(Position {
line: line_idx as u32,
character: utf16_count as u32,
})
}
}
}
}
#[derive(Debug, Default)]
struct DocumentsInner {
by_uri: HashMap<Uri, Document>,
encoding: PositionEncoding,
}
#[derive(Debug, Clone, Default)]
pub struct Documents {
inner: Arc<RwLock<DocumentsInner>>,
}
impl Documents {
pub fn new() -> Self {
Self::default()
}
pub fn open(&self, item: TextDocumentItem) {
let mut inner = self.inner.write().unwrap();
inner.by_uri.insert(
item.uri.clone(),
Document {
uri: item.uri,
language_id: item.language_id,
version: item.version,
text: Rope::from_str(&item.text),
},
);
}
pub fn get(&self, uri: &Uri) -> Option<Document> {
let inner = self.inner.read().unwrap();
inner.by_uri.get(uri).cloned()
}
pub fn close(&self, uri: &Uri) -> Option<Document> {
let mut inner = self.inner.write().unwrap();
inner.by_uri.remove(uri)
}
pub fn save(&self, uri: &Uri) -> Option<()> {
let inner = self.inner.read().unwrap();
inner.by_uri.contains_key(uri).then_some(())
}
pub fn apply_incremental_change(
&self,
uri: &Uri,
version: i32,
change: TextDocumentContentChangeEvent,
) -> crate::Result<()> {
let mut inner = self.inner.write().unwrap();
let encoding = inner.encoding;
let doc = inner
.by_uri
.get_mut(uri)
.ok_or_else(|| crate::LspError::invalid_request("document not found"))?;
if let Some(range) = change.range {
let start_offset = doc
.position_to_offset(encoding, range.start)
.ok_or_else(|| crate::LspError::invalid_request("invalid start position"))?;
let end_offset = doc
.position_to_offset(encoding, range.end)
.ok_or_else(|| crate::LspError::invalid_request("invalid end position"))?;
if start_offset > end_offset {
return Err(
crate::LspError::invalid_request("range end precedes range start").into(),
);
}
let start_char = doc.text.byte_to_char(start_offset);
let end_char = doc.text.byte_to_char(end_offset);
doc.text.remove(start_char..end_char);
doc.text.insert(start_char, &change.text);
} else {
doc.text = Rope::from_str(&change.text);
}
doc.version = version;
Ok(())
}
pub fn position_to_offset(&self, uri: &Uri, position: Position) -> Option<usize> {
let inner = self.inner.read().unwrap();
inner
.by_uri
.get(uri)
.and_then(|doc| doc.position_to_offset(inner.encoding, position))
}
pub fn offset_to_position(&self, uri: &Uri, offset: usize) -> Option<Position> {
let inner = self.inner.read().unwrap();
inner
.by_uri
.get(uri)
.and_then(|doc| doc.offset_to_position(inner.encoding, offset))
}
pub fn position_encoding(&self) -> PositionEncoding {
self.inner.read().unwrap().encoding
}
pub fn set_position_encoding(&self, encoding: PositionEncoding) {
self.inner.write().unwrap().encoding = encoding;
}
}