Skip to main content

rustledger_lsp/
vfs.rs

1//! Virtual File System for document management.
2//!
3//! The VFS maintains the in-memory state of all open documents,
4//! handling incremental updates from the editor.
5//!
6//! Documents cache their parse results to avoid re-parsing on every request.
7
8use ropey::Rope;
9use rustledger_parser::{ParseResult, parse};
10use std::collections::HashMap;
11use std::path::PathBuf;
12use std::sync::Arc;
13
14/// A document in the virtual file system.
15#[derive(Debug)]
16pub struct Document {
17    /// The document content as a rope for efficient editing.
18    content: Rope,
19    /// The document version (incremented on each change).
20    version: i32,
21    /// Cached parse result (lazily computed, invalidated on change).
22    parse_cache: Option<Arc<ParseResult>>,
23}
24
25impl Document {
26    /// Create a new document with the given content.
27    pub fn new(content: String, version: i32) -> Self {
28        Self {
29            content: Rope::from_str(&content),
30            version,
31            parse_cache: None,
32        }
33    }
34
35    /// Get the document content as a string.
36    pub fn text(&self) -> String {
37        self.content.to_string()
38    }
39
40    /// Get the document version.
41    pub fn version(&self) -> i32 {
42        self.version
43    }
44
45    /// Get or compute the parse result (cached).
46    pub fn parse_result(&mut self) -> Arc<ParseResult> {
47        if self.parse_cache.is_none() {
48            let text = self.content.to_string();
49            self.parse_cache = Some(Arc::new(parse(&text)));
50        }
51        self.parse_cache.clone().unwrap()
52    }
53
54    /// Invalidate the parse cache (called on content change).
55    fn invalidate_cache(&mut self) {
56        self.parse_cache = None;
57    }
58
59    /// Update the document content.
60    pub fn update(&mut self, content: String, version: i32) {
61        self.content = Rope::from_str(&content);
62        self.version = version;
63        self.invalidate_cache();
64    }
65}
66
67/// Virtual file system for managing open documents.
68#[derive(Debug, Default)]
69pub struct Vfs {
70    /// Open documents indexed by path.
71    documents: HashMap<PathBuf, Document>,
72}
73
74impl Vfs {
75    /// Create a new empty VFS.
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Open a document in the VFS.
81    pub fn open(&mut self, path: PathBuf, content: String, version: i32) {
82        self.documents.insert(path, Document::new(content, version));
83    }
84
85    /// Close a document in the VFS.
86    pub fn close(&mut self, path: &PathBuf) {
87        self.documents.remove(path);
88    }
89
90    /// Get a document by path (immutable).
91    pub fn get(&self, path: &PathBuf) -> Option<&Document> {
92        self.documents.get(path)
93    }
94
95    /// Get a document by path (mutable, for parse caching).
96    pub fn get_mut(&mut self, path: &PathBuf) -> Option<&mut Document> {
97        self.documents.get_mut(path)
98    }
99
100    /// Get document content as a string.
101    pub fn get_content(&self, path: &PathBuf) -> Option<String> {
102        self.documents.get(path).map(|d| d.text())
103    }
104
105    /// Get document content and cached parse result.
106    /// This is the preferred method for request handlers.
107    pub fn get_document_data(&mut self, path: &PathBuf) -> Option<(String, Arc<ParseResult>)> {
108        self.documents.get_mut(path).map(|doc| {
109            let text = doc.text();
110            let parse_result = doc.parse_result();
111            (text, parse_result)
112        })
113    }
114
115    /// Update a document's content.
116    pub fn update(&mut self, path: &PathBuf, content: String, version: i32) {
117        if let Some(doc) = self.documents.get_mut(path) {
118            doc.update(content, version);
119        }
120    }
121
122    /// Get all open document paths.
123    pub fn paths(&self) -> impl Iterator<Item = &PathBuf> {
124        self.documents.keys()
125    }
126
127    /// Iterate over all open documents (path and content).
128    pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, String)> {
129        self.documents.iter().map(|(path, doc)| (path, doc.text()))
130    }
131
132    /// Iterate over all open documents with parse results.
133    pub fn iter_with_parse(
134        &mut self,
135    ) -> impl Iterator<Item = (&PathBuf, String, Arc<ParseResult>)> {
136        self.documents.iter_mut().map(|(path, doc)| {
137            let text = doc.text();
138            let parse_result = doc.parse_result();
139            (path, text, parse_result)
140        })
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_vfs_open_close() {
150        let mut vfs = Vfs::new();
151        let path = PathBuf::from("/test.beancount");
152
153        vfs.open(path.clone(), "2024-01-01 open Assets:Bank".to_string(), 1);
154        assert!(vfs.get(&path).is_some());
155
156        vfs.close(&path);
157        assert!(vfs.get(&path).is_none());
158    }
159
160    #[test]
161    fn test_document_text() {
162        let doc = Document::new("hello world".to_string(), 1);
163        assert_eq!(doc.text(), "hello world");
164    }
165}