1#![allow(dead_code)]
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum EditScope {
7 Local,
9 Track,
11 Global,
13}
14
15impl EditScope {
16 #[must_use]
18 pub fn affects_downstream(&self) -> bool {
19 matches!(self, EditScope::Track | EditScope::Global)
20 }
21
22 #[must_use]
24 pub fn label(&self) -> &'static str {
25 match self {
26 EditScope::Local => "Local",
27 EditScope::Track => "Track",
28 EditScope::Global => "Global",
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct EditContext {
36 pub id: u64,
38 pub description: String,
40 pub scope: EditScope,
42 pub reversible: bool,
44 pub created_at_ms: u64,
46}
47
48impl EditContext {
49 pub fn new(
51 id: u64,
52 description: impl Into<String>,
53 scope: EditScope,
54 reversible: bool,
55 created_at_ms: u64,
56 ) -> Self {
57 Self {
58 id,
59 description: description.into(),
60 scope,
61 reversible,
62 created_at_ms,
63 }
64 }
65
66 #[must_use]
68 pub fn has_undo(&self) -> bool {
69 self.reversible
70 }
71}
72
73#[derive(Debug, Default)]
75pub struct EditContextManager {
76 stack: Vec<EditContext>,
77 next_id: u64,
78}
79
80impl EditContextManager {
81 #[must_use]
83 pub fn new() -> Self {
84 Self {
85 stack: Vec::new(),
86 next_id: 1,
87 }
88 }
89
90 pub fn push_context(
92 &mut self,
93 description: impl Into<String>,
94 scope: EditScope,
95 reversible: bool,
96 timestamp_ms: u64,
97 ) -> u64 {
98 let id = self.next_id;
99 self.next_id += 1;
100 self.stack.push(EditContext::new(
101 id,
102 description,
103 scope,
104 reversible,
105 timestamp_ms,
106 ));
107 id
108 }
109
110 pub fn pop_context(&mut self) -> Option<EditContext> {
112 self.stack.pop()
113 }
114
115 #[must_use]
117 pub fn current(&self) -> Option<&EditContext> {
118 self.stack.last()
119 }
120
121 #[must_use]
123 pub fn depth(&self) -> usize {
124 self.stack.len()
125 }
126
127 #[must_use]
129 pub fn reversible_contexts(&self) -> Vec<&EditContext> {
130 self.stack.iter().rev().filter(|c| c.reversible).collect()
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_scope_local_not_downstream() {
140 assert!(!EditScope::Local.affects_downstream());
141 }
142
143 #[test]
144 fn test_scope_track_downstream() {
145 assert!(EditScope::Track.affects_downstream());
146 }
147
148 #[test]
149 fn test_scope_global_downstream() {
150 assert!(EditScope::Global.affects_downstream());
151 }
152
153 #[test]
154 fn test_scope_labels() {
155 assert_eq!(EditScope::Local.label(), "Local");
156 assert_eq!(EditScope::Track.label(), "Track");
157 assert_eq!(EditScope::Global.label(), "Global");
158 }
159
160 #[test]
161 fn test_edit_context_has_undo_true() {
162 let ctx = EditContext::new(1, "Trim", EditScope::Local, true, 0);
163 assert!(ctx.has_undo());
164 }
165
166 #[test]
167 fn test_edit_context_has_undo_false() {
168 let ctx = EditContext::new(2, "Export", EditScope::Global, false, 0);
169 assert!(!ctx.has_undo());
170 }
171
172 #[test]
173 fn test_manager_new_empty() {
174 let mgr = EditContextManager::new();
175 assert_eq!(mgr.depth(), 0);
176 assert!(mgr.current().is_none());
177 }
178
179 #[test]
180 fn test_manager_push_returns_id() {
181 let mut mgr = EditContextManager::new();
182 let id = mgr.push_context("Cut", EditScope::Local, true, 100);
183 assert_eq!(id, 1);
184 let id2 = mgr.push_context("Ripple", EditScope::Track, true, 200);
185 assert_eq!(id2, 2);
186 }
187
188 #[test]
189 fn test_manager_current_is_latest() {
190 let mut mgr = EditContextManager::new();
191 mgr.push_context("First", EditScope::Local, true, 0);
192 mgr.push_context("Second", EditScope::Track, false, 1);
193 let cur = mgr.current().expect("cur should be valid");
194 assert_eq!(cur.description, "Second");
195 }
196
197 #[test]
198 fn test_manager_pop_returns_context() {
199 let mut mgr = EditContextManager::new();
200 mgr.push_context("Op", EditScope::Global, true, 50);
201 let popped = mgr.pop_context().expect("popped should be valid");
202 assert_eq!(popped.description, "Op");
203 assert_eq!(mgr.depth(), 0);
204 }
205
206 #[test]
207 fn test_manager_pop_empty_is_none() {
208 let mut mgr = EditContextManager::new();
209 assert!(mgr.pop_context().is_none());
210 }
211
212 #[test]
213 fn test_manager_depth_grows() {
214 let mut mgr = EditContextManager::new();
215 for i in 0..5 {
216 mgr.push_context(format!("Op{i}"), EditScope::Local, true, i as u64);
217 }
218 assert_eq!(mgr.depth(), 5);
219 }
220
221 #[test]
222 fn test_reversible_contexts_filtered() {
223 let mut mgr = EditContextManager::new();
224 mgr.push_context("A", EditScope::Local, true, 0);
225 mgr.push_context("B", EditScope::Track, false, 1);
226 mgr.push_context("C", EditScope::Global, true, 2);
227 let rev = mgr.reversible_contexts();
228 assert_eq!(rev.len(), 2);
229 assert_eq!(rev[0].description, "C");
231 assert_eq!(rev[1].description, "A");
232 }
233
234 #[test]
235 fn test_context_scope_stored() {
236 let ctx = EditContext::new(10, "Test", EditScope::Track, true, 999);
237 assert_eq!(ctx.scope, EditScope::Track);
238 assert_eq!(ctx.id, 10);
239 }
240}