Skip to main content

shape_lsp/
document.rs

1//! Document management for LSP
2//!
3//! Tracks open documents, their content, and version numbers for incremental updates.
4
5use crate::module_cache::ModuleCache;
6use crate::symbols::SymbolInfo;
7use dashmap::DashMap;
8use ropey::Rope;
9use std::collections::HashMap;
10use std::path::PathBuf;
11use std::sync::Arc;
12use tower_lsp_server::ls_types::Uri;
13
14/// A document in the workspace with its content and metadata
15#[derive(Debug, Clone)]
16pub struct Document {
17    /// The URI of the document
18    pub uri: Uri,
19    /// The version number (from LSP protocol)
20    pub version: i32,
21    /// The text content as a Rope for efficient editing
22    pub rope: Rope,
23    /// Cached symbols from last successful parse (for completion fallback)
24    pub cached_symbols: Vec<SymbolInfo>,
25    /// Cached type info from last successful inference (for completion fallback)
26    pub cached_types: HashMap<String, String>,
27}
28
29impl Document {
30    /// Create a new document
31    pub fn new(uri: Uri, version: i32, text: String) -> Self {
32        Self {
33            uri,
34            version,
35            rope: Rope::from_str(&text),
36            cached_symbols: Vec::new(),
37            cached_types: HashMap::new(),
38        }
39    }
40
41    /// Update cached symbols from successful parse
42    pub fn update_cached_symbols(&mut self, symbols: Vec<SymbolInfo>) {
43        self.cached_symbols = symbols;
44    }
45
46    /// Update cached type info from successful inference
47    pub fn update_cached_types(&mut self, types: HashMap<String, String>) {
48        self.cached_types = types;
49    }
50
51    /// Get cached symbols
52    pub fn get_cached_symbols(&self) -> &[SymbolInfo] {
53        &self.cached_symbols
54    }
55
56    /// Get cached type info
57    pub fn get_cached_types(&self) -> &HashMap<String, String> {
58        &self.cached_types
59    }
60
61    /// Get the full text content
62    pub fn text(&self) -> String {
63        self.rope.to_string()
64    }
65
66    /// Get the number of lines
67    pub fn line_count(&self) -> usize {
68        self.rope.len_lines()
69    }
70
71    /// Get a specific line (0-indexed)
72    pub fn line(&self, line_idx: usize) -> Option<String> {
73        if line_idx >= self.line_count() {
74            return None;
75        }
76
77        let start = self.rope.line_to_char(line_idx);
78        let end = if line_idx + 1 < self.line_count() {
79            self.rope.line_to_char(line_idx + 1)
80        } else {
81            self.rope.len_chars()
82        };
83
84        Some(self.rope.slice(start..end).to_string())
85    }
86
87    /// Convert LSP Position to byte offset
88    pub fn position_to_offset(&self, line: u32, character: u32) -> Option<usize> {
89        let line_idx = line as usize;
90        if line_idx >= self.line_count() {
91            return None;
92        }
93
94        let line_start = self.rope.line_to_char(line_idx);
95        let offset = line_start + character as usize;
96
97        if offset > self.rope.len_chars() {
98            return None;
99        }
100
101        Some(offset)
102    }
103
104    /// Convert byte offset to LSP Position
105    pub fn offset_to_position(&self, offset: usize) -> Option<(u32, u32)> {
106        if offset > self.rope.len_chars() {
107            return None;
108        }
109
110        let line = self.rope.char_to_line(offset);
111        let line_start = self.rope.line_to_char(line);
112        let column = offset - line_start;
113
114        Some((line as u32, column as u32))
115    }
116}
117
118/// Manages all open documents in the workspace
119#[derive(Debug)]
120pub struct DocumentManager {
121    /// Map of URI to Document
122    documents: DashMap<Uri, Document>,
123    /// Module cache for cross-file navigation
124    module_cache: Arc<ModuleCache>,
125}
126
127impl Default for DocumentManager {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl DocumentManager {
134    /// Create a new document manager
135    pub fn new() -> Self {
136        Self {
137            documents: DashMap::new(),
138            module_cache: Arc::new(ModuleCache::new()),
139        }
140    }
141
142    /// Get the module cache
143    pub fn get_module_cache(&self) -> Arc<ModuleCache> {
144        self.module_cache.clone()
145    }
146
147    /// Open a new document
148    pub fn open(&self, uri: Uri, version: i32, text: String) {
149        let doc = Document::new(uri.clone(), version, text);
150        self.documents.insert(uri, doc);
151    }
152
153    /// Close a document
154    pub fn close(&self, uri: &Uri) {
155        // Invalidate module cache for this file
156        let path = PathBuf::from(uri.path().as_str());
157        self.module_cache.invalidate(&path);
158
159        self.documents.remove(uri);
160    }
161
162    /// Update document content (full update)
163    pub fn update(&self, uri: &Uri, version: i32, text: String) {
164        // Invalidate module cache for this file since it changed
165        let path = PathBuf::from(uri.path().as_str());
166        self.module_cache.invalidate(&path);
167
168        if let Some(mut doc) = self.documents.get_mut(uri) {
169            doc.version = version;
170            doc.rope = Rope::from_str(&text);
171        }
172    }
173
174    /// Get a document by URI
175    pub fn get(&self, uri: &Uri) -> Option<Document> {
176        self.documents.get(uri).map(|doc| doc.clone())
177    }
178
179    /// Check if a document is open
180    pub fn contains(&self, uri: &Uri) -> bool {
181        self.documents.contains_key(uri)
182    }
183
184    /// Get all document URIs
185    pub fn all_uris(&self) -> Vec<Uri> {
186        self.documents
187            .iter()
188            .map(|entry| entry.key().clone())
189            .collect()
190    }
191
192    /// Update cached symbols for a document
193    pub fn update_cached_symbols(&self, uri: &Uri, symbols: Vec<SymbolInfo>) {
194        if let Some(mut doc) = self.documents.get_mut(uri) {
195            doc.update_cached_symbols(symbols);
196        }
197    }
198
199    /// Update cached type info for a document
200    pub fn update_cached_types(&self, uri: &Uri, types: HashMap<String, String>) {
201        if let Some(mut doc) = self.documents.get_mut(uri) {
202            doc.update_cached_types(types);
203        }
204    }
205
206    /// Get cached symbols for a document
207    pub fn get_cached_symbols(&self, uri: &Uri) -> Vec<SymbolInfo> {
208        self.documents
209            .get(uri)
210            .map(|doc| doc.get_cached_symbols().to_vec())
211            .unwrap_or_default()
212    }
213
214    /// Get cached type info for a document
215    pub fn get_cached_types(&self, uri: &Uri) -> HashMap<String, String> {
216        self.documents
217            .get(uri)
218            .map(|doc| doc.get_cached_types().clone())
219            .unwrap_or_default()
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_document_creation() {
229        let uri = Uri::from_file_path("/test.shape").unwrap();
230        let doc = Document::new(uri.clone(), 1, "let x = 5;\nlet y = 10;".to_string());
231
232        assert_eq!(doc.version, 1);
233        assert_eq!(doc.line_count(), 2);
234        assert_eq!(doc.text(), "let x = 5;\nlet y = 10;");
235    }
236
237    #[test]
238    fn test_position_conversion() {
239        let uri = Uri::from_file_path("/test.shape").unwrap();
240        let doc = Document::new(uri, 1, "let x = 5;\nlet y = 10;".to_string());
241
242        // Test position to offset
243        let offset = doc.position_to_offset(0, 4).unwrap();
244        assert_eq!(doc.text().chars().nth(offset), Some('x'));
245
246        // Test offset to position
247        let (line, col) = doc.offset_to_position(4).unwrap();
248        assert_eq!(line, 0);
249        assert_eq!(col, 4);
250    }
251
252    #[test]
253    fn test_document_manager() {
254        let manager = DocumentManager::new();
255        let uri = Uri::from_file_path("/test.shape").unwrap();
256
257        // Open document
258        manager.open(uri.clone(), 1, "let x = 5;".to_string());
259        assert!(manager.contains(&uri));
260
261        // Get document
262        let doc = manager.get(&uri).unwrap();
263        assert_eq!(doc.version, 1);
264        assert_eq!(doc.text(), "let x = 5;");
265
266        // Update document
267        manager.update(&uri, 2, "let x = 10;".to_string());
268        let doc = manager.get(&uri).unwrap();
269        assert_eq!(doc.version, 2);
270        assert_eq!(doc.text(), "let x = 10;");
271
272        // Close document
273        manager.close(&uri);
274        assert!(!manager.contains(&uri));
275    }
276}