codebook_lsp/
file_cache.rs1use std::{collections::HashMap, sync::RwLock};
2
3use log::debug;
4use tower_lsp::lsp_types::{TextDocumentItem, Url};
5
6#[derive(Debug, Clone)]
7pub struct TextDocumentCacheItem {
8 pub text: String,
9 pub uri: Url,
10 pub version: Option<i32>,
11 pub language_id: Option<String>,
12}
13
14impl TextDocumentCacheItem {
15 pub fn new(
16 uri: &Url,
17 version: Option<i32>,
18 language_id: Option<&str>,
19 text: Option<&str>,
20 ) -> Self {
21 Self {
22 uri: uri.clone(),
23 version,
24 language_id: language_id.map(|id| id.to_string()),
25 text: match text {
26 Some(text) => text.to_string(),
27 None => String::new(),
28 },
29 }
30 }
31}
32
33#[derive(Debug, Default)]
38pub struct TextDocumentCache {
39 documents: RwLock<HashMap<String, TextDocumentCacheItem>>,
40}
41
42impl TextDocumentCache {
43 pub fn get(&self, uri: &str) -> Option<TextDocumentCacheItem> {
44 self.documents.read().unwrap().get(uri).cloned()
45 }
46
47 pub fn insert(&self, document: &TextDocumentItem) {
48 let document = TextDocumentCacheItem::new(
49 &document.uri,
50 Some(document.version),
51 Some(&document.language_id),
52 Some(&document.text),
53 );
54 self.documents
55 .write()
56 .unwrap()
57 .insert(document.uri.to_string(), document);
58 }
59
60 pub fn update(&self, uri: &Url, text: &str, version: Option<i32>) {
65 let key = uri.to_string();
66 let mut cache = self.documents.write().unwrap();
67 match cache.get_mut(&key) {
68 Some(item) => {
69 item.text = text.to_string();
70 if version.is_some() {
71 item.version = version;
72 }
73 }
74 None => {
75 debug!("Ignoring update for document that was never opened: {uri}");
76 }
77 }
78 }
79
80 pub fn remove(&self, uri: &Url) {
81 self.documents.write().unwrap().remove(uri.as_str());
82 }
83
84 pub fn cached_urls(&self) -> Vec<Url> {
85 self.documents
86 .read()
87 .unwrap()
88 .values()
89 .map(|v| v.uri.clone())
90 .collect()
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 fn doc(uri: &Url) -> TextDocumentItem {
99 TextDocumentItem {
100 uri: uri.clone(),
101 language_id: "rust".to_string(),
102 version: 1,
103 text: "hello".to_string(),
104 }
105 }
106
107 #[test]
108 fn test_lifecycle_bounds_cache() {
109 let cache = TextDocumentCache::default();
110 let uri = Url::parse("file:///a.rs").unwrap();
111
112 cache.insert(&doc(&uri));
113 assert_eq!(cache.cached_urls().len(), 1);
114
115 cache.remove(&uri);
116 assert!(cache.cached_urls().is_empty());
117 assert!(cache.get(uri.as_str()).is_none());
118 }
119
120 #[test]
121 fn test_update_changes_text_and_version() {
122 let cache = TextDocumentCache::default();
123 let uri = Url::parse("file:///a.rs").unwrap();
124 cache.insert(&doc(&uri));
125
126 cache.update(&uri, "changed", Some(2));
127 let item = cache.get(uri.as_str()).unwrap();
128 assert_eq!(item.text, "changed");
129 assert_eq!(item.version, Some(2));
130
131 cache.update(&uri, "saved", None);
133 let item = cache.get(uri.as_str()).unwrap();
134 assert_eq!(item.text, "saved");
135 assert_eq!(item.version, Some(2));
136 }
137
138 #[test]
139 fn test_update_for_unopened_document_is_dropped() {
140 let cache = TextDocumentCache::default();
141 let uri = Url::parse("file:///never-opened.rs").unwrap();
142
143 cache.update(&uri, "text", Some(1));
144
145 assert!(cache.get(uri.as_str()).is_none());
147 assert!(cache.cached_urls().is_empty());
148 }
149}