use dashmap::DashMap;
use lsp_types::{TextDocumentContentChangeEvent, Url};
pub struct DocumentManager {
documents: DashMap<Url, Document>,
}
#[derive(Debug, Clone)]
pub struct Document {
pub uri: Url,
pub version: i32,
pub content: String,
pub language_id: String,
}
impl DocumentManager {
pub fn new() -> Self {
Self {
documents: DashMap::new(),
}
}
pub fn open(&self, uri: Url, version: i32, content: String, language_id: String) {
let doc = Document {
uri: uri.clone(),
version,
content,
language_id,
};
self.documents.insert(uri, doc);
}
pub fn change(&self, uri: &Url, version: i32, changes: Vec<TextDocumentContentChangeEvent>) {
if let Some(mut doc) = self.documents.get_mut(uri) {
doc.version = version;
for change in changes {
if let Some(range) = change.range {
if let Err(e) =
Self::apply_incremental_change(&mut doc.content, range, &change.text)
{
tracing::error!("Failed to apply incremental change: {}", e);
doc.content = change.text;
}
} else {
doc.content = change.text;
}
}
}
}
fn apply_incremental_change(
content: &mut String,
range: lsp_types::Range,
text: &str,
) -> Result<(), String> {
let lines: Vec<&str> = content.lines().collect();
let start_line = range.start.line as usize;
let end_line = range.end.line as usize;
if start_line > lines.len() || end_line > lines.len() {
return Err(format!(
"Invalid range: start_line={}, end_line={}, total_lines={}",
start_line,
end_line,
lines.len()
));
}
let start_offset = Self::position_to_offset(content, range.start)?;
let end_offset = Self::position_to_offset(content, range.end)?;
if start_offset > end_offset || end_offset > content.len() {
return Err(format!(
"Invalid offsets: start={}, end={}, content_len={}",
start_offset,
end_offset,
content.len()
));
}
let mut new_content =
String::with_capacity(content.len() - (end_offset - start_offset) + text.len());
new_content.push_str(&content[..start_offset]);
new_content.push_str(text);
new_content.push_str(&content[end_offset..]);
*content = new_content;
Ok(())
}
fn position_to_offset(content: &str, position: lsp_types::Position) -> Result<usize, String> {
let mut offset = 0;
let mut current_line = 0;
let target_line = position.line as usize;
let target_char = position.character as usize;
for line in content.lines() {
if current_line == target_line {
let char_offset = Self::char_offset_to_byte_offset(line, target_char)?;
return Ok(offset + char_offset);
}
offset += line.len() + 1; current_line += 1;
}
if current_line == target_line && target_char == 0 {
return Ok(offset);
}
Err(format!(
"Position out of bounds: line={}, char={}, total_lines={}",
target_line, target_char, current_line
))
}
fn char_offset_to_byte_offset(line: &str, char_offset: usize) -> Result<usize, String> {
let mut byte_offset = 0;
let mut current_char = 0;
for ch in line.chars() {
if current_char == char_offset {
return Ok(byte_offset);
}
byte_offset += ch.len_utf8();
current_char += 1;
}
if current_char == char_offset {
return Ok(byte_offset);
}
Err(format!(
"Character offset out of bounds: char_offset={}, line_length={}",
char_offset, current_char
))
}
pub fn close(&self, uri: &Url) {
self.documents.remove(uri);
}
pub fn get(&self, uri: &Url) -> Option<Document> {
self.documents.get(uri).map(|doc| doc.clone())
}
pub fn with_document<F, R>(&self, uri: &Url, f: F) -> Option<R>
where
F: FnOnce(&Document) -> R,
{
self.documents.get(uri).map(|doc| f(&doc))
}
}
impl Default for DocumentManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use lsp_types::{Position, Range};
#[test]
fn test_open_document() {
let manager = DocumentManager::new();
let uri: Url = "file:///test.toml".parse().unwrap();
manager.open(uri.clone(), 1, "content".to_string(), "toml".to_string());
let doc = manager.get(&uri).unwrap();
assert_eq!(doc.version, 1);
assert_eq!(doc.content, "content");
assert_eq!(doc.language_id, "toml");
}
#[test]
fn test_close_document() {
let manager = DocumentManager::new();
let uri: Url = "file:///test.toml".parse().unwrap();
manager.open(uri.clone(), 1, "content".to_string(), "toml".to_string());
assert!(manager.get(&uri).is_some());
manager.close(&uri);
assert!(manager.get(&uri).is_none());
}
#[test]
fn test_full_content_change() {
let manager = DocumentManager::new();
let uri: Url = "file:///test.toml".parse().unwrap();
manager.open(
uri.clone(),
1,
"old content".to_string(),
"toml".to_string(),
);
let changes = vec![TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "new content".to_string(),
}];
manager.change(&uri, 2, changes);
let doc = manager.get(&uri).unwrap();
assert_eq!(doc.version, 2);
assert_eq!(doc.content, "new content");
}
#[test]
fn test_incremental_change_single_line() {
let manager = DocumentManager::new();
let uri: Url = "file:///test.toml".parse().unwrap();
manager.open(
uri.clone(),
1,
"hello world".to_string(),
"toml".to_string(),
);
let changes = vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 0,
character: 6,
},
end: Position {
line: 0,
character: 11,
},
}),
range_length: None,
text: "rust".to_string(),
}];
manager.change(&uri, 2, changes);
let doc = manager.get(&uri).unwrap();
assert_eq!(doc.content, "hello rust");
}
#[test]
fn test_incremental_change_multiline() {
let manager = DocumentManager::new();
let uri: Url = "file:///test.toml".parse().unwrap();
let initial_content = "line 1\nline 2\nline 3";
manager.open(
uri.clone(),
1,
initial_content.to_string(),
"toml".to_string(),
);
let changes = vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 1,
character: 0,
},
end: Position {
line: 1,
character: 6,
},
}),
range_length: None,
text: "modified".to_string(),
}];
manager.change(&uri, 2, changes);
let doc = manager.get(&uri).unwrap();
assert_eq!(doc.content, "line 1\nmodified\nline 3");
}
#[test]
fn test_incremental_change_insert() {
let manager = DocumentManager::new();
let uri: Url = "file:///test.toml".parse().unwrap();
manager.open(uri.clone(), 1, "hello".to_string(), "toml".to_string());
let changes = vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 0,
character: 5,
},
}),
range_length: None,
text: " world".to_string(),
}];
manager.change(&uri, 2, changes);
let doc = manager.get(&uri).unwrap();
assert_eq!(doc.content, "hello world");
}
#[test]
fn test_incremental_change_delete() {
let manager = DocumentManager::new();
let uri: Url = "file:///test.toml".parse().unwrap();
manager.open(
uri.clone(),
1,
"hello world".to_string(),
"toml".to_string(),
);
let changes = vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 0,
character: 5,
},
end: Position {
line: 0,
character: 11,
},
}),
range_length: None,
text: "".to_string(),
}];
manager.change(&uri, 2, changes);
let doc = manager.get(&uri).unwrap();
assert_eq!(doc.content, "hello");
}
#[test]
fn test_incremental_change_utf8() {
let manager = DocumentManager::new();
let uri: Url = "file:///test.toml".parse().unwrap();
manager.open(uri.clone(), 1, "你好世界".to_string(), "toml".to_string());
let changes = vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 0,
character: 2,
},
end: Position {
line: 0,
character: 4,
},
}),
range_length: None,
text: "Rust".to_string(),
}];
manager.change(&uri, 2, changes);
let doc = manager.get(&uri).unwrap();
assert_eq!(doc.content, "你好Rust");
}
#[test]
fn test_with_document() {
let manager = DocumentManager::new();
let uri: Url = "file:///test.toml".parse().unwrap();
manager.open(uri.clone(), 1, "content".to_string(), "toml".to_string());
let length = manager.with_document(&uri, |doc| doc.content.len());
assert_eq!(length, Some(7));
let not_found = manager.with_document(&"file:///notfound.toml".parse().unwrap(), |doc| {
doc.content.len()
});
assert_eq!(not_found, None);
}
#[test]
fn test_position_to_offset() {
let content = "line 1\nline 2\nline 3";
let offset = DocumentManager::position_to_offset(
content,
Position {
line: 0,
character: 0,
},
)
.unwrap();
assert_eq!(offset, 0);
let offset = DocumentManager::position_to_offset(
content,
Position {
line: 0,
character: 4,
},
)
.unwrap();
assert_eq!(offset, 4);
let offset = DocumentManager::position_to_offset(
content,
Position {
line: 1,
character: 0,
},
)
.unwrap();
assert_eq!(offset, 7);
let offset = DocumentManager::position_to_offset(
content,
Position {
line: 2,
character: 0,
},
)
.unwrap();
assert_eq!(offset, 14); }
#[test]
fn test_char_offset_to_byte_offset() {
let line = "hello";
let offset = DocumentManager::char_offset_to_byte_offset(line, 2).unwrap();
assert_eq!(offset, 2);
let line = "你好世界";
let offset = DocumentManager::char_offset_to_byte_offset(line, 2).unwrap();
assert_eq!(offset, 6);
let offset = DocumentManager::char_offset_to_byte_offset(line, 4).unwrap();
assert_eq!(offset, 12);
}
#[test]
fn test_multiple_changes() {
let manager = DocumentManager::new();
let uri: Url = "file:///test.toml".parse().unwrap();
manager.open(uri.clone(), 1, "a b c".to_string(), "toml".to_string());
let changes = vec![
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 1,
},
}),
range_length: None,
text: "x".to_string(),
},
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 0,
character: 2,
},
end: Position {
line: 0,
character: 3,
},
}),
range_length: None,
text: "y".to_string(),
},
];
manager.change(&uri, 2, changes);
let doc = manager.get(&uri).unwrap();
assert_eq!(doc.content, "x y c");
}
}