1use dashmap::DashMap;
4use lsp_types::{TextDocumentContentChangeEvent, Url};
5
6pub struct DocumentManager {
8 documents: DashMap<Url, Document>,
10}
11
12#[derive(Debug, Clone)]
14pub struct Document {
15 pub uri: Url,
17 pub version: i32,
19 pub content: String,
21 pub language_id: String,
23}
24
25impl DocumentManager {
26 pub fn new() -> Self {
28 Self {
29 documents: DashMap::new(),
30 }
31 }
32
33 pub fn open(&self, uri: Url, version: i32, content: String, language_id: String) {
35 let doc = Document {
36 uri: uri.clone(),
37 version,
38 content,
39 language_id,
40 };
41 self.documents.insert(uri, doc);
42 }
43
44 pub fn change(&self, uri: &Url, version: i32, changes: Vec<TextDocumentContentChangeEvent>) {
46 if let Some(mut doc) = self.documents.get_mut(uri) {
47 doc.version = version;
48
49 for change in changes {
51 if let Some(range) = change.range {
52 if let Err(e) =
54 Self::apply_incremental_change(&mut doc.content, range, &change.text)
55 {
56 tracing::error!("Failed to apply incremental change: {}", e);
57 doc.content = change.text;
59 }
60 } else {
61 doc.content = change.text;
63 }
64 }
65 }
66 }
67
68 fn apply_incremental_change(
70 content: &mut String,
71 range: lsp_types::Range,
72 text: &str,
73 ) -> Result<(), String> {
74 let lines: Vec<&str> = content.lines().collect();
76
77 let start_line = range.start.line as usize;
79 let end_line = range.end.line as usize;
80
81 if start_line > lines.len() || end_line > lines.len() {
82 return Err(format!(
83 "Invalid range: start_line={}, end_line={}, total_lines={}",
84 start_line,
85 end_line,
86 lines.len()
87 ));
88 }
89
90 let start_offset = Self::position_to_offset(content, range.start)?;
92 let end_offset = Self::position_to_offset(content, range.end)?;
93
94 if start_offset > end_offset || end_offset > content.len() {
95 return Err(format!(
96 "Invalid offsets: start={}, end={}, content_len={}",
97 start_offset,
98 end_offset,
99 content.len()
100 ));
101 }
102
103 let mut new_content =
105 String::with_capacity(content.len() - (end_offset - start_offset) + text.len());
106 new_content.push_str(&content[..start_offset]);
107 new_content.push_str(text);
108 new_content.push_str(&content[end_offset..]);
109
110 *content = new_content;
111 Ok(())
112 }
113
114 fn position_to_offset(content: &str, position: lsp_types::Position) -> Result<usize, String> {
116 let mut offset = 0;
117 let mut current_line = 0;
118 let target_line = position.line as usize;
119 let target_char = position.character as usize;
120
121 for line in content.lines() {
122 if current_line == target_line {
123 let char_offset = Self::char_offset_to_byte_offset(line, target_char)?;
125 return Ok(offset + char_offset);
126 }
127
128 offset += line.len() + 1; current_line += 1;
131 }
132
133 if current_line == target_line && target_char == 0 {
135 return Ok(offset);
136 }
137
138 Err(format!(
139 "Position out of bounds: line={}, char={}, total_lines={}",
140 target_line, target_char, current_line
141 ))
142 }
143
144 fn char_offset_to_byte_offset(line: &str, char_offset: usize) -> Result<usize, String> {
146 let mut byte_offset = 0;
147 let mut current_char = 0;
148
149 for ch in line.chars() {
150 if current_char == char_offset {
151 return Ok(byte_offset);
152 }
153 byte_offset += ch.len_utf8();
154 current_char += 1;
155 }
156
157 if current_char == char_offset {
159 return Ok(byte_offset);
160 }
161
162 Err(format!(
163 "Character offset out of bounds: char_offset={}, line_length={}",
164 char_offset, current_char
165 ))
166 }
167
168 pub fn close(&self, uri: &Url) {
170 self.documents.remove(uri);
171 }
172
173 pub fn get(&self, uri: &Url) -> Option<Document> {
175 self.documents.get(uri).map(|doc| doc.clone())
176 }
177
178 pub fn with_document<F, R>(&self, uri: &Url, f: F) -> Option<R>
180 where
181 F: FnOnce(&Document) -> R,
182 {
183 self.documents.get(uri).map(|doc| f(&doc))
184 }
185}
186
187impl Default for DocumentManager {
188 fn default() -> Self {
189 Self::new()
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use lsp_types::{Position, Range};
197
198 #[test]
199 fn test_open_document() {
200 let manager = DocumentManager::new();
201 let uri: Url = "file:///test.toml".parse().unwrap();
202
203 manager.open(uri.clone(), 1, "content".to_string(), "toml".to_string());
204
205 let doc = manager.get(&uri).unwrap();
206 assert_eq!(doc.version, 1);
207 assert_eq!(doc.content, "content");
208 assert_eq!(doc.language_id, "toml");
209 }
210
211 #[test]
212 fn test_close_document() {
213 let manager = DocumentManager::new();
214 let uri: Url = "file:///test.toml".parse().unwrap();
215
216 manager.open(uri.clone(), 1, "content".to_string(), "toml".to_string());
217 assert!(manager.get(&uri).is_some());
218
219 manager.close(&uri);
220 assert!(manager.get(&uri).is_none());
221 }
222
223 #[test]
224 fn test_full_content_change() {
225 let manager = DocumentManager::new();
226 let uri: Url = "file:///test.toml".parse().unwrap();
227
228 manager.open(
229 uri.clone(),
230 1,
231 "old content".to_string(),
232 "toml".to_string(),
233 );
234
235 let changes = vec![TextDocumentContentChangeEvent {
236 range: None,
237 range_length: None,
238 text: "new content".to_string(),
239 }];
240
241 manager.change(&uri, 2, changes);
242
243 let doc = manager.get(&uri).unwrap();
244 assert_eq!(doc.version, 2);
245 assert_eq!(doc.content, "new content");
246 }
247
248 #[test]
249 fn test_incremental_change_single_line() {
250 let manager = DocumentManager::new();
251 let uri: Url = "file:///test.toml".parse().unwrap();
252
253 manager.open(
254 uri.clone(),
255 1,
256 "hello world".to_string(),
257 "toml".to_string(),
258 );
259
260 let changes = vec![TextDocumentContentChangeEvent {
262 range: Some(Range {
263 start: Position {
264 line: 0,
265 character: 6,
266 },
267 end: Position {
268 line: 0,
269 character: 11,
270 },
271 }),
272 range_length: None,
273 text: "rust".to_string(),
274 }];
275
276 manager.change(&uri, 2, changes);
277
278 let doc = manager.get(&uri).unwrap();
279 assert_eq!(doc.content, "hello rust");
280 }
281
282 #[test]
283 fn test_incremental_change_multiline() {
284 let manager = DocumentManager::new();
285 let uri: Url = "file:///test.toml".parse().unwrap();
286
287 let initial_content = "line 1\nline 2\nline 3";
288 manager.open(
289 uri.clone(),
290 1,
291 initial_content.to_string(),
292 "toml".to_string(),
293 );
294
295 let changes = vec![TextDocumentContentChangeEvent {
297 range: Some(Range {
298 start: Position {
299 line: 1,
300 character: 0,
301 },
302 end: Position {
303 line: 1,
304 character: 6,
305 },
306 }),
307 range_length: None,
308 text: "modified".to_string(),
309 }];
310
311 manager.change(&uri, 2, changes);
312
313 let doc = manager.get(&uri).unwrap();
314 assert_eq!(doc.content, "line 1\nmodified\nline 3");
315 }
316
317 #[test]
318 fn test_incremental_change_insert() {
319 let manager = DocumentManager::new();
320 let uri: Url = "file:///test.toml".parse().unwrap();
321
322 manager.open(uri.clone(), 1, "hello".to_string(), "toml".to_string());
323
324 let changes = vec![TextDocumentContentChangeEvent {
326 range: Some(Range {
327 start: Position {
328 line: 0,
329 character: 5,
330 },
331 end: Position {
332 line: 0,
333 character: 5,
334 },
335 }),
336 range_length: None,
337 text: " world".to_string(),
338 }];
339
340 manager.change(&uri, 2, changes);
341
342 let doc = manager.get(&uri).unwrap();
343 assert_eq!(doc.content, "hello world");
344 }
345
346 #[test]
347 fn test_incremental_change_delete() {
348 let manager = DocumentManager::new();
349 let uri: Url = "file:///test.toml".parse().unwrap();
350
351 manager.open(
352 uri.clone(),
353 1,
354 "hello world".to_string(),
355 "toml".to_string(),
356 );
357
358 let changes = vec![TextDocumentContentChangeEvent {
360 range: Some(Range {
361 start: Position {
362 line: 0,
363 character: 5,
364 },
365 end: Position {
366 line: 0,
367 character: 11,
368 },
369 }),
370 range_length: None,
371 text: "".to_string(),
372 }];
373
374 manager.change(&uri, 2, changes);
375
376 let doc = manager.get(&uri).unwrap();
377 assert_eq!(doc.content, "hello");
378 }
379
380 #[test]
381 fn test_incremental_change_utf8() {
382 let manager = DocumentManager::new();
383 let uri: Url = "file:///test.toml".parse().unwrap();
384
385 manager.open(uri.clone(), 1, "你好世界".to_string(), "toml".to_string());
386
387 let changes = vec![TextDocumentContentChangeEvent {
389 range: Some(Range {
390 start: Position {
391 line: 0,
392 character: 2,
393 },
394 end: Position {
395 line: 0,
396 character: 4,
397 },
398 }),
399 range_length: None,
400 text: "Rust".to_string(),
401 }];
402
403 manager.change(&uri, 2, changes);
404
405 let doc = manager.get(&uri).unwrap();
406 assert_eq!(doc.content, "你好Rust");
407 }
408
409 #[test]
410 fn test_with_document() {
411 let manager = DocumentManager::new();
412 let uri: Url = "file:///test.toml".parse().unwrap();
413
414 manager.open(uri.clone(), 1, "content".to_string(), "toml".to_string());
415
416 let length = manager.with_document(&uri, |doc| doc.content.len());
417 assert_eq!(length, Some(7));
418
419 let not_found = manager.with_document(&"file:///notfound.toml".parse().unwrap(), |doc| {
420 doc.content.len()
421 });
422 assert_eq!(not_found, None);
423 }
424
425 #[test]
426 fn test_position_to_offset() {
427 let content = "line 1\nline 2\nline 3";
428
429 let offset = DocumentManager::position_to_offset(
431 content,
432 Position {
433 line: 0,
434 character: 0,
435 },
436 )
437 .unwrap();
438 assert_eq!(offset, 0);
439
440 let offset = DocumentManager::position_to_offset(
442 content,
443 Position {
444 line: 0,
445 character: 4,
446 },
447 )
448 .unwrap();
449 assert_eq!(offset, 4);
450
451 let offset = DocumentManager::position_to_offset(
453 content,
454 Position {
455 line: 1,
456 character: 0,
457 },
458 )
459 .unwrap();
460 assert_eq!(offset, 7); let offset = DocumentManager::position_to_offset(
464 content,
465 Position {
466 line: 2,
467 character: 0,
468 },
469 )
470 .unwrap();
471 assert_eq!(offset, 14); }
473
474 #[test]
475 fn test_char_offset_to_byte_offset() {
476 let line = "hello";
478 let offset = DocumentManager::char_offset_to_byte_offset(line, 2).unwrap();
479 assert_eq!(offset, 2);
480
481 let line = "你好世界";
483 let offset = DocumentManager::char_offset_to_byte_offset(line, 2).unwrap();
484 assert_eq!(offset, 6); let offset = DocumentManager::char_offset_to_byte_offset(line, 4).unwrap();
488 assert_eq!(offset, 12);
489 }
490
491 #[test]
492 fn test_multiple_changes() {
493 let manager = DocumentManager::new();
494 let uri: Url = "file:///test.toml".parse().unwrap();
495
496 manager.open(uri.clone(), 1, "a b c".to_string(), "toml".to_string());
497
498 let changes = vec![
500 TextDocumentContentChangeEvent {
501 range: Some(Range {
502 start: Position {
503 line: 0,
504 character: 0,
505 },
506 end: Position {
507 line: 0,
508 character: 1,
509 },
510 }),
511 range_length: None,
512 text: "x".to_string(),
513 },
514 TextDocumentContentChangeEvent {
515 range: Some(Range {
516 start: Position {
517 line: 0,
518 character: 2,
519 },
520 end: Position {
521 line: 0,
522 character: 3,
523 },
524 }),
525 range_length: None,
526 text: "y".to_string(),
527 },
528 ];
529
530 manager.change(&uri, 2, changes);
531
532 let doc = manager.get(&uri).unwrap();
533 assert_eq!(doc.content, "x y c");
537 }
538}