ftui_runtime/undo/
transaction.rs1#![forbid(unsafe_code)]
2
3use std::fmt;
56
57use super::command::{CommandBatch, CommandError, CommandResult, UndoableCmd};
58use super::history::HistoryManager;
59
60pub struct Transaction {
68 batch: CommandBatch,
70 executed_count: usize,
72 finalized: bool,
74}
75
76impl fmt::Debug for Transaction {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 f.debug_struct("Transaction")
79 .field("description", &self.batch.description())
80 .field("command_count", &self.batch.len())
81 .field("executed_count", &self.executed_count)
82 .field("finalized", &self.finalized)
83 .finish()
84 }
85}
86
87impl Transaction {
88 #[must_use]
90 pub fn begin(description: impl Into<String>) -> Self {
91 Self {
92 batch: CommandBatch::new(description),
93 executed_count: 0,
94 finalized: false,
95 }
96 }
97
98 pub fn execute(&mut self, mut cmd: Box<dyn UndoableCmd>) -> CommandResult {
107 if self.finalized {
108 return Err(CommandError::InvalidState(
109 "transaction already finalized".to_string(),
110 ));
111 }
112
113 if let Err(e) = cmd.execute() {
115 self.rollback();
117 return Err(e);
118 }
119
120 self.executed_count += 1;
122 self.batch.push_executed(cmd);
123 Ok(())
124 }
125
126 pub fn add_executed(&mut self, cmd: Box<dyn UndoableCmd>) -> CommandResult {
135 if self.finalized {
136 return Err(CommandError::InvalidState(
137 "transaction already finalized".to_string(),
138 ));
139 }
140
141 self.executed_count += 1;
142 self.batch.push_executed(cmd);
143 Ok(())
144 }
145
146 #[must_use]
150 pub fn commit(mut self) -> Option<Box<dyn UndoableCmd>> {
151 self.finalized = true;
152
153 if self.batch.is_empty() {
154 None
155 } else {
156 let batch = std::mem::replace(&mut self.batch, CommandBatch::new(""));
160 Some(Box::new(batch))
161 }
162 }
163
164 pub fn rollback(&mut self) {
169 if self.finalized {
170 return;
171 }
172
173 self.finalized = true;
185
186 if self.executed_count > 0 {
188 let _ = self.batch.undo();
190 }
191 }
192
193 #[must_use]
195 pub fn is_empty(&self) -> bool {
196 self.batch.is_empty()
197 }
198
199 #[must_use]
201 pub fn len(&self) -> usize {
202 self.batch.len()
203 }
204
205 #[must_use]
207 pub fn description(&self) -> &str {
208 self.batch.description()
209 }
210}
211
212impl Drop for Transaction {
213 fn drop(&mut self) {
214 if !self.finalized {
216 self.rollback();
217 }
218 }
219}
220
221pub struct TransactionScope<'a> {
227 history: &'a mut HistoryManager,
229 stack: Vec<Transaction>,
231}
232
233impl<'a> TransactionScope<'a> {
234 #[must_use]
236 pub fn new(history: &'a mut HistoryManager) -> Self {
237 Self {
238 history,
239 stack: Vec::new(),
240 }
241 }
242
243 pub fn begin(&mut self, description: impl Into<String>) {
245 self.stack.push(Transaction::begin(description));
246 }
247
248 pub fn execute(&mut self, cmd: Box<dyn UndoableCmd>) -> CommandResult {
257 if let Some(txn) = self.stack.last_mut() {
258 txn.execute(cmd)
259 } else {
260 let mut cmd = cmd;
262 cmd.execute()?;
263 self.history.push(cmd);
264 Ok(())
265 }
266 }
267
268 pub fn commit(&mut self) -> CommandResult {
277 let txn = self
278 .stack
279 .pop()
280 .ok_or_else(|| CommandError::InvalidState("no active transaction".to_string()))?;
281
282 if let Some(cmd) = txn.commit() {
283 if let Some(parent) = self.stack.last_mut() {
284 parent.add_executed(cmd)?;
286 } else {
287 self.history.push(cmd);
289 }
290 }
291
292 Ok(())
293 }
294
295 pub fn rollback(&mut self) -> CommandResult {
301 let mut txn = self
302 .stack
303 .pop()
304 .ok_or_else(|| CommandError::InvalidState("no active transaction".to_string()))?;
305
306 txn.rollback();
307 Ok(())
308 }
309
310 #[must_use]
312 pub fn is_active(&self) -> bool {
313 !self.stack.is_empty()
314 }
315
316 #[must_use]
318 pub fn depth(&self) -> usize {
319 self.stack.len()
320 }
321}
322
323impl Drop for TransactionScope<'_> {
324 fn drop(&mut self) {
325 while let Some(mut txn) = self.stack.pop() {
327 txn.rollback();
328 }
329 }
330}
331
332#[cfg(test)]
337mod tests {
338 use super::*;
339 use crate::undo::command::{TextInsertCmd, WidgetId};
340 use crate::undo::history::HistoryConfig;
341 use std::sync::Arc;
342 use std::sync::Mutex;
343
344 fn make_cmd(buffer: Arc<Mutex<String>>, text: &str) -> Box<dyn UndoableCmd> {
346 let b1 = buffer.clone();
347 let b2 = buffer.clone();
348 let text = text.to_string();
349 let text_clone = text.clone();
350
351 let mut cmd = TextInsertCmd::new(WidgetId::new(1), 0, text)
352 .with_apply(move |_, _, txt| {
353 let mut buf = b1.lock().unwrap();
354 buf.push_str(txt);
355 Ok(())
356 })
357 .with_remove(move |_, _, _| {
358 let mut buf = b2.lock().unwrap();
359 buf.drain(..text_clone.len());
360 Ok(())
361 });
362
363 cmd.execute().unwrap();
364 Box::new(cmd)
365 }
366
367 #[test]
368 fn test_empty_transaction() {
369 let txn = Transaction::begin("Empty");
370 assert!(txn.is_empty());
371 assert_eq!(txn.len(), 0);
372 assert!(txn.commit().is_none());
373 }
374
375 #[test]
376 fn test_single_command_transaction() {
377 let buffer = Arc::new(Mutex::new(String::new()));
378
379 let mut txn = Transaction::begin("Single");
380 txn.add_executed(make_cmd(buffer.clone(), "hello")).unwrap();
381
382 assert_eq!(txn.len(), 1);
383
384 let cmd = txn.commit();
385 assert!(cmd.is_some());
386 }
387
388 #[test]
389 fn test_transaction_rollback() {
390 let buffer = Arc::new(Mutex::new(String::new()));
391
392 let mut txn = Transaction::begin("Rollback Test");
393 txn.add_executed(make_cmd(buffer.clone(), "hello")).unwrap();
394 txn.add_executed(make_cmd(buffer.clone(), " world"))
395 .unwrap();
396
397 assert_eq!(*buffer.lock().unwrap(), "hello world");
398
399 txn.rollback();
400
401 assert_eq!(*buffer.lock().unwrap(), "");
403 }
404
405 #[test]
406 fn test_transaction_commit_to_history() {
407 let buffer = Arc::new(Mutex::new(String::new()));
408 let mut history = HistoryManager::new(HistoryConfig::unlimited());
409
410 let mut txn = Transaction::begin("Commit Test");
411 txn.add_executed(make_cmd(buffer.clone(), "a")).unwrap();
412 txn.add_executed(make_cmd(buffer.clone(), "b")).unwrap();
413
414 if let Some(cmd) = txn.commit() {
415 history.push(cmd);
416 }
417
418 assert_eq!(history.undo_depth(), 1);
419 assert!(history.can_undo());
420 }
421
422 #[test]
423 fn test_transaction_undo_redo() {
424 let buffer = Arc::new(Mutex::new(String::new()));
425 let mut history = HistoryManager::new(HistoryConfig::unlimited());
426
427 let mut txn = Transaction::begin("Undo/Redo Test");
428 txn.add_executed(make_cmd(buffer.clone(), "hello")).unwrap();
429 txn.add_executed(make_cmd(buffer.clone(), " world"))
430 .unwrap();
431
432 if let Some(cmd) = txn.commit() {
433 history.push(cmd);
434 }
435
436 assert_eq!(*buffer.lock().unwrap(), "hello world");
437
438 history.undo();
440 assert_eq!(*buffer.lock().unwrap(), "");
441
442 history.redo();
444 assert_eq!(*buffer.lock().unwrap(), "hello world");
445 }
446
447 #[test]
448 fn test_scope_basic() {
449 let buffer = Arc::new(Mutex::new(String::new()));
450 let mut history = HistoryManager::new(HistoryConfig::unlimited());
451
452 {
453 let mut scope = TransactionScope::new(&mut history);
454 scope.begin("Scope Test");
455
456 scope.execute(make_cmd(buffer.clone(), "a")).unwrap();
457 scope.execute(make_cmd(buffer.clone(), "b")).unwrap();
458
459 scope.commit().unwrap();
460 }
461
462 assert_eq!(history.undo_depth(), 1);
463 }
464
465 #[test]
466 fn test_scope_nested() {
467 let buffer = Arc::new(Mutex::new(String::new()));
468 let mut history = HistoryManager::new(HistoryConfig::unlimited());
469
470 {
471 let mut scope = TransactionScope::new(&mut history);
472
473 scope.begin("Outer");
475 scope.execute(make_cmd(buffer.clone(), "outer1")).unwrap();
476
477 scope.begin("Inner");
479 scope.execute(make_cmd(buffer.clone(), "inner")).unwrap();
480 scope.commit().unwrap();
481
482 scope.execute(make_cmd(buffer.clone(), "outer2")).unwrap();
483 scope.commit().unwrap();
484 }
485
486 assert_eq!(history.undo_depth(), 1);
488 }
489
490 #[test]
491 fn test_scope_rollback() {
492 let buffer = Arc::new(Mutex::new(String::new()));
493 let mut history = HistoryManager::new(HistoryConfig::unlimited());
494
495 {
496 let mut scope = TransactionScope::new(&mut history);
497 scope.begin("Rollback");
498
499 scope.execute(make_cmd(buffer.clone(), "a")).unwrap();
500 scope.execute(make_cmd(buffer.clone(), "b")).unwrap();
501
502 scope.rollback().unwrap();
503 }
504
505 assert_eq!(history.undo_depth(), 0);
507 }
508
509 #[test]
510 fn test_scope_auto_rollback_on_drop() {
511 let buffer = Arc::new(Mutex::new(String::new()));
512 let mut history = HistoryManager::new(HistoryConfig::unlimited());
513
514 {
515 let mut scope = TransactionScope::new(&mut history);
516 scope.begin("Will be dropped");
517 scope.execute(make_cmd(buffer.clone(), "test")).unwrap();
518 }
520
521 assert_eq!(history.undo_depth(), 0);
523 }
524
525 #[test]
526 fn test_scope_depth() {
527 let mut history = HistoryManager::new(HistoryConfig::unlimited());
528
529 let mut scope = TransactionScope::new(&mut history);
530 assert_eq!(scope.depth(), 0);
531 assert!(!scope.is_active());
532
533 scope.begin("Level 1");
534 assert_eq!(scope.depth(), 1);
535 assert!(scope.is_active());
536
537 scope.begin("Level 2");
538 assert_eq!(scope.depth(), 2);
539
540 scope.commit().unwrap();
541 assert_eq!(scope.depth(), 1);
542
543 scope.commit().unwrap();
544 assert_eq!(scope.depth(), 0);
545 assert!(!scope.is_active());
546 }
547
548 #[test]
549 fn test_transaction_description() {
550 let txn = Transaction::begin("My Transaction");
551 assert_eq!(txn.description(), "My Transaction");
552 }
553
554 #[test]
555 fn test_finalized_transaction_rejects_commands() {
556 let buffer = Arc::new(Mutex::new(String::new()));
557
558 let mut txn = Transaction::begin("Finalized");
559 txn.rollback();
560
561 let result = txn.add_executed(make_cmd(buffer, "test"));
562 assert!(result.is_err());
563 }
564}