Skip to main content

spring_lsp/
document.rs

1//! 文档管理模块
2
3use dashmap::DashMap;
4use lsp_types::{TextDocumentContentChangeEvent, Url};
5
6/// 文档管理器
7pub struct DocumentManager {
8    /// 文档缓存(DashMap 本身就是并发安全的)
9    documents: DashMap<Url, Document>,
10}
11
12/// 文档
13#[derive(Debug, Clone)]
14pub struct Document {
15    /// 文档 URI
16    pub uri: Url,
17    /// 文档版本
18    pub version: i32,
19    /// 文档内容
20    pub content: String,
21    /// 语言 ID
22    pub language_id: String,
23}
24
25impl DocumentManager {
26    /// 创建新的文档管理器
27    pub fn new() -> Self {
28        Self {
29            documents: DashMap::new(),
30        }
31    }
32
33    /// 打开文档
34    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    /// 修改文档
45    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            // 应用修改
50            for change in changes {
51                if let Some(range) = change.range {
52                    // 增量修改
53                    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                        // 降级到全量更新
58                        doc.content = change.text;
59                    }
60                } else {
61                    // 全量修改
62                    doc.content = change.text;
63                }
64            }
65        }
66    }
67
68    /// 应用增量修改
69    fn apply_incremental_change(
70        content: &mut String,
71        range: lsp_types::Range,
72        text: &str,
73    ) -> Result<(), String> {
74        // 将内容按行分割
75        let lines: Vec<&str> = content.lines().collect();
76
77        // 验证范围的有效性
78        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        // 计算起始和结束位置的字节偏移
91        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        // 构建新内容
104        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    /// 将 LSP Position 转换为字节偏移
115    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                // 找到目标行,计算字符偏移
124                let char_offset = Self::char_offset_to_byte_offset(line, target_char)?;
125                return Ok(offset + char_offset);
126            }
127
128            // 移动到下一行(包括换行符)
129            offset += line.len() + 1; // +1 for '\n'
130            current_line += 1;
131        }
132
133        // 如果到达文件末尾
134        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    /// 将字符偏移转换为字节偏移
145    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        // 允许在行尾
158        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    /// 关闭文档
169    pub fn close(&self, uri: &Url) {
170        self.documents.remove(uri);
171    }
172
173    /// 获取文档(返回克隆以避免锁竞争)
174    pub fn get(&self, uri: &Url) -> Option<Document> {
175        self.documents.get(uri).map(|doc| doc.clone())
176    }
177
178    /// 获取文档的只读引用(用于快速访问)
179    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        // 替换 "world" 为 "rust"
261        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        // 替换第二行的 "line 2" 为 "modified"
296        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        // 在 "hello" 后插入 " world"
325        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        // 删除 " world"
359        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        // 替换 "世界" 为 "Rust"
388        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        // 第一行开始
430        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        // 第一行 "line" 后
441        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        // 第二行开始
452        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); // "line 1\n"
461
462        // 第三行开始
463        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); // "line 1\nline 2\n"
472    }
473
474    #[test]
475    fn test_char_offset_to_byte_offset() {
476        // ASCII
477        let line = "hello";
478        let offset = DocumentManager::char_offset_to_byte_offset(line, 2).unwrap();
479        assert_eq!(offset, 2);
480
481        // UTF-8
482        let line = "你好世界";
483        let offset = DocumentManager::char_offset_to_byte_offset(line, 2).unwrap();
484        assert_eq!(offset, 6); // 每个中文字符 3 字节
485
486        // 行尾
487        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        // 应用多个修改
499        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        // 注意:LSP 的修改是按顺序应用的,每次修改后文档内容会改变
534        // 第一次修改: "a b c" -> "x b c"
535        // 第二次修改: "x b c" -> "x y c"
536        assert_eq!(doc.content, "x y c");
537    }
538}