use dashmap::DashMap;
use ropey::Rope;
use tower_lsp::lsp_types::{TextDocumentContentChangeEvent, Url};
use crate::utils::position_to_offset;
#[derive(Debug)]
pub struct Document {
pub uri: Url,
pub version: i32,
pub content: Rope,
pub language_id: String,
}
impl Document {
pub fn new(uri: Url, content: String, version: i32, language_id: String) -> Self {
Self {
uri,
version,
content: Rope::from_str(&content),
language_id,
}
}
pub fn text(&self) -> String {
self.content.to_string()
}
pub fn line_count(&self) -> usize {
self.content.len_lines()
}
pub fn line(&self, line_idx: usize) -> Option<String> {
if line_idx >= self.content.len_lines() {
return None;
}
Some(self.content.line(line_idx).to_string())
}
pub fn apply_change(&mut self, change: &TextDocumentContentChangeEvent, new_version: i32) {
self.version = new_version;
if let Some(range) = change.range {
let start_offset = position_to_offset(&self.content, range.start);
let end_offset = position_to_offset(&self.content, range.end);
if let (Some(start), Some(end)) = (start_offset, end_offset) {
if let (Ok(start_char), Ok(end_char)) = (
self.content.try_byte_to_char(start),
self.content.try_byte_to_char(end),
) {
self.content.remove(start_char..end_char);
self.content.insert(start_char, &change.text);
}
}
} else {
self.content = Rope::from_str(&change.text);
}
}
}
pub struct DocumentStore {
documents: DashMap<Url, Document>,
}
impl Default for DocumentStore {
fn default() -> Self {
Self::new()
}
}
impl DocumentStore {
pub fn new() -> Self {
Self {
documents: DashMap::new(),
}
}
pub fn open(&self, uri: Url, content: String, version: i32, language_id: String) {
let doc = Document::new(uri.clone(), content, version, language_id);
self.documents.insert(uri, doc);
}
pub fn close(&self, uri: &Url) {
self.documents.remove(uri);
}
pub fn get(&self, uri: &Url) -> Option<dashmap::mapref::one::Ref<'_, Url, Document>> {
self.documents.get(uri)
}
pub fn get_mut(&self, uri: &Url) -> Option<dashmap::mapref::one::RefMut<'_, Url, Document>> {
self.documents.get_mut(uri)
}
pub fn apply_changes(
&self,
uri: &Url,
changes: Vec<TextDocumentContentChangeEvent>,
version: i32,
) {
if let Some(mut doc) = self.documents.get_mut(uri) {
for change in changes {
doc.apply_change(&change, version);
}
}
}
pub fn contains(&self, uri: &Url) -> bool {
self.documents.contains_key(uri)
}
pub fn uris(&self) -> Vec<Url> {
self.documents.iter().map(|r| r.key().clone()).collect()
}
pub fn len(&self) -> usize {
self.documents.len()
}
pub fn is_empty(&self) -> bool {
self.documents.is_empty()
}
pub fn iter(&self) -> dashmap::iter::Iter<'_, Url, Document> {
self.documents.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tower_lsp::lsp_types::{Position, Range};
fn test_uri() -> Url {
Url::parse("file:///test.vue").unwrap()
}
#[test]
fn test_document_creation() {
let doc = Document::new(test_uri(), "hello world".to_string(), 1, "vue".to_string());
assert_eq!(doc.text(), "hello world");
assert_eq!(doc.version, 1);
assert_eq!(doc.language_id, "vue");
}
#[test]
fn test_document_line_count() {
let doc = Document::new(
test_uri(),
"line1\nline2\nline3".to_string(),
1,
"vue".to_string(),
);
assert_eq!(doc.line_count(), 3);
}
#[test]
fn test_document_get_line() {
let doc = Document::new(
test_uri(),
"line1\nline2\nline3".to_string(),
1,
"vue".to_string(),
);
assert_eq!(doc.line(0), Some("line1\n".to_string()));
assert_eq!(doc.line(1), Some("line2\n".to_string()));
assert_eq!(doc.line(2), Some("line3".to_string()));
assert_eq!(doc.line(3), None);
}
#[test]
fn test_incremental_change() {
let mut doc = Document::new(test_uri(), "hello world".to_string(), 1, "vue".to_string());
let change = TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 0,
character: 6,
},
end: Position {
line: 0,
character: 11,
},
}),
range_length: None,
text: "universe".to_string(),
};
doc.apply_change(&change, 2);
assert_eq!(doc.text(), "hello universe");
assert_eq!(doc.version, 2);
}
#[test]
fn test_full_content_change() {
let mut doc = Document::new(test_uri(), "hello world".to_string(), 1, "vue".to_string());
let change = TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "completely new content".to_string(),
};
doc.apply_change(&change, 2);
assert_eq!(doc.text(), "completely new content");
}
#[test]
fn test_document_store() {
let store = DocumentStore::new();
store.open(test_uri(), "content".to_string(), 1, "vue".to_string());
assert!(store.contains(&test_uri()));
assert_eq!(store.len(), 1);
{
let doc = store.get(&test_uri()).unwrap();
assert_eq!(doc.text(), "content");
}
store.close(&test_uri());
assert!(!store.contains(&test_uri()));
assert!(store.is_empty());
}
}