lattice_agent/
state_cache.rs1use std::collections::HashMap;
17use std::path::PathBuf;
18use std::sync::{Arc, Mutex};
19
20use lattice_protocol::ids::DocumentId;
21use lattice_protocol::{Event, EventKind, SelectionSet};
22use lattice_runtime::{EventBus, EventFilter, SubscriptionTarget};
23
24#[derive(Debug, Clone, Default)]
26pub struct OpenBuffer {
27 pub path: Option<PathBuf>,
30 pub version: u64,
32}
33
34#[derive(Debug, Clone)]
36pub struct ActiveSelection {
37 pub buffer: DocumentId,
39 pub version: u64,
41 pub selections: SelectionSet,
43}
44
45#[derive(Debug, Default)]
50pub struct EditorStateCache {
51 pub open_buffers: HashMap<DocumentId, OpenBuffer>,
53 pub active: Option<ActiveSelection>,
55}
56
57impl EditorStateCache {
58 pub fn apply_event(&mut self, event: &Event) {
62 match event {
63 Event::DocumentOpened {
64 id, path, version, ..
65 } => {
66 self.open_buffers.insert(
67 *id,
68 OpenBuffer {
69 path: path.clone(),
70 version: *version,
71 },
72 );
73 }
74 Event::DocumentClosed { id } => {
75 self.open_buffers.remove(id);
76 if self.active.as_ref().is_some_and(|a| a.buffer == *id) {
77 self.active = None;
78 }
79 }
80 Event::SelectionsChanged {
81 id,
82 version,
83 selections,
84 } => {
85 if let Some(b) = self.open_buffers.get_mut(id) {
86 b.version = *version;
87 }
88 self.active = Some(ActiveSelection {
89 buffer: *id,
90 version: *version,
91 selections: selections.clone(),
92 });
93 }
94 _ => {}
95 }
96 }
97}
98
99pub type EditorStateHandle = Arc<Mutex<EditorStateCache>>;
104
105pub fn spawn_read_cache(bus: &Arc<EventBus>, rt: &tokio::runtime::Handle) -> EditorStateHandle {
109 let cache: EditorStateHandle = Arc::new(Mutex::new(EditorStateCache::default()));
110 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Event>();
111 bus.subscribe(
112 EventFilter::kinds(vec![
113 EventKind::DocumentOpened,
114 EventKind::DocumentClosed,
115 EventKind::SelectionsChanged,
116 ]),
117 SubscriptionTarget::Channel(tx),
118 );
119 let cache_for_task = Arc::clone(&cache);
120 rt.spawn(async move {
121 while let Some(event) = rx.recv().await {
122 cache_for_task
123 .lock()
124 .unwrap_or_else(|e| e.into_inner())
125 .apply_event(&event);
126 }
127 });
128 cache
129}
130
131#[cfg(test)]
132mod tests {
133 #![allow(clippy::unwrap_used)]
134 use super::*;
135
136 fn doc(id: u64) -> DocumentId {
137 DocumentId::new(id)
138 }
139
140 fn opened(id: u64, path: &str) -> Event {
141 Event::DocumentOpened {
142 id: doc(id),
143 path: Some(PathBuf::from(path)),
144 version: 1,
145 text: String::new(),
146 }
147 }
148
149 #[test]
150 fn document_opened_adds_to_open_set_with_path() {
151 let mut s = EditorStateCache::default();
152 s.apply_event(&opened(1, "/work/a.rs"));
153 assert_eq!(s.open_buffers.len(), 1);
154 assert_eq!(
155 s.open_buffers[&doc(1)].path.as_deref(),
156 Some(std::path::Path::new("/work/a.rs"))
157 );
158 }
159
160 #[test]
161 fn selections_changed_sets_active_buffer_and_version() {
162 let mut s = EditorStateCache::default();
163 s.apply_event(&opened(1, "/work/a.rs"));
164 s.apply_event(&Event::SelectionsChanged {
165 id: doc(1),
166 version: 7,
167 selections: SelectionSet::default(),
168 });
169 let active = s.active.as_ref().expect("active set");
170 assert_eq!(active.buffer, doc(1));
171 assert_eq!(active.version, 7);
172 }
173
174 #[test]
175 fn document_closed_removes_from_set_and_clears_active() {
176 let mut s = EditorStateCache::default();
177 s.apply_event(&opened(1, "/work/a.rs"));
178 s.apply_event(&Event::SelectionsChanged {
179 id: doc(1),
180 version: 2,
181 selections: SelectionSet::default(),
182 });
183 assert!(s.active.is_some());
184 s.apply_event(&Event::DocumentClosed { id: doc(1) });
185 assert!(s.open_buffers.is_empty());
186 assert!(
187 s.active.is_none(),
188 "closing the active buffer clears active"
189 );
190 }
191
192 #[test]
193 fn closing_a_non_active_buffer_keeps_active() {
194 let mut s = EditorStateCache::default();
195 s.apply_event(&opened(1, "/work/a.rs"));
196 s.apply_event(&opened(2, "/work/b.rs"));
197 s.apply_event(&Event::SelectionsChanged {
198 id: doc(2),
199 version: 3,
200 selections: SelectionSet::default(),
201 });
202 s.apply_event(&Event::DocumentClosed { id: doc(1) });
203 assert_eq!(s.open_buffers.len(), 1);
204 assert!(
205 s.active.is_some(),
206 "closing a non-active buffer leaves active"
207 );
208 }
209
210 #[test]
211 fn unrelated_event_is_a_noop() {
212 let mut s = EditorStateCache::default();
213 s.apply_event(&Event::DocumentSaved {
214 id: doc(1),
215 path: PathBuf::from("/work/a.rs"),
216 });
217 assert!(s.open_buffers.is_empty());
218 assert!(s.active.is_none());
219 }
220}