1#![forbid(unsafe_code)]
2
3use std::any::Any;
32use std::fmt;
33use std::time::Instant;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub struct WidgetId(pub u64);
41
42impl WidgetId {
43 #[must_use]
45 pub const fn new(id: u64) -> Self {
46 Self(id)
47 }
48
49 #[must_use]
51 pub const fn raw(self) -> u64 {
52 self.0
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
60pub enum CommandSource {
61 #[default]
63 User,
64 Programmatic,
66 Macro,
68 External,
70}
71
72#[derive(Debug, Clone)]
74pub struct CommandMetadata {
75 pub description: String,
77 pub timestamp: Instant,
79 pub source: CommandSource,
81 pub batch_id: Option<u64>,
83}
84
85impl CommandMetadata {
86 #[must_use]
88 pub fn new(description: impl Into<String>) -> Self {
89 Self {
90 description: description.into(),
91 timestamp: Instant::now(),
92 source: CommandSource::User,
93 batch_id: None,
94 }
95 }
96
97 #[must_use]
99 pub fn with_source(mut self, source: CommandSource) -> Self {
100 self.source = source;
101 self
102 }
103
104 #[must_use]
106 pub fn with_batch(mut self, batch_id: u64) -> Self {
107 self.batch_id = Some(batch_id);
108 self
109 }
110
111 #[must_use]
113 pub fn size_bytes(&self) -> usize {
114 std::mem::size_of::<Self>() + self.description.len()
115 }
116}
117
118impl Default for CommandMetadata {
119 fn default() -> Self {
120 Self::new("Unknown")
121 }
122}
123
124pub type CommandResult = Result<(), CommandError>;
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum CommandError {
132 TargetNotFound(WidgetId),
134 PositionOutOfBounds { position: usize, length: usize },
136 StateDrift { expected: String, actual: String },
138 InvalidState(String),
140 Other(String),
142}
143
144impl fmt::Display for CommandError {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 Self::TargetNotFound(id) => write!(f, "target widget {:?} not found", id),
148 Self::PositionOutOfBounds { position, length } => {
149 write!(f, "position {} out of bounds (length {})", position, length)
150 }
151 Self::StateDrift { expected, actual } => {
152 write!(f, "state drift: expected '{}', got '{}'", expected, actual)
153 }
154 Self::InvalidState(msg) => write!(f, "invalid state: {}", msg),
155 Self::Other(msg) => write!(f, "{}", msg),
156 }
157 }
158}
159
160impl std::error::Error for CommandError {}
161
162#[derive(Debug, Clone, Copy)]
164pub struct MergeConfig {
165 pub max_delay_ms: u64,
167 pub merge_across_words: bool,
169 pub max_merged_size: usize,
171}
172
173impl Default for MergeConfig {
174 fn default() -> Self {
175 Self {
176 max_delay_ms: 500,
177 merge_across_words: false,
178 max_merged_size: 1024,
179 }
180 }
181}
182
183pub trait UndoableCmd: Send + Sync {
188 fn execute(&mut self) -> CommandResult;
190
191 fn undo(&mut self) -> CommandResult;
193
194 fn redo(&mut self) -> CommandResult {
196 self.execute()
197 }
198
199 fn description(&self) -> &str;
201
202 fn size_bytes(&self) -> usize;
204
205 fn can_merge(&self, _other: &dyn UndoableCmd, _config: &MergeConfig) -> bool {
207 false
208 }
209
210 fn merge_text(&self) -> Option<&str> {
215 None
216 }
217
218 fn accept_merge(&mut self, _other: &dyn UndoableCmd) -> bool {
223 false
224 }
225
226 fn metadata(&self) -> &CommandMetadata;
228
229 fn target(&self) -> Option<WidgetId> {
231 None
232 }
233
234 fn as_any(&self) -> &dyn Any;
236
237 fn as_any_mut(&mut self) -> &mut dyn Any;
239
240 fn debug_name(&self) -> &'static str {
242 "UndoableCmd"
243 }
244}
245
246impl fmt::Debug for dyn UndoableCmd {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 f.debug_struct(self.debug_name())
249 .field("description", &self.description())
250 .field("size_bytes", &self.size_bytes())
251 .finish()
252 }
253}
254
255pub struct CommandBatch {
260 commands: Vec<Box<dyn UndoableCmd>>,
262 metadata: CommandMetadata,
264 executed_to: usize,
266}
267
268impl fmt::Debug for CommandBatch {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 f.debug_struct("CommandBatch")
271 .field("commands_count", &self.commands.len())
272 .field("metadata", &self.metadata)
273 .field("executed_to", &self.executed_to)
274 .finish()
275 }
276}
277
278impl CommandBatch {
279 #[must_use]
281 pub fn new(description: impl Into<String>) -> Self {
282 Self {
283 commands: Vec::new(),
284 metadata: CommandMetadata::new(description),
285 executed_to: 0,
286 }
287 }
288
289 pub fn push(&mut self, cmd: Box<dyn UndoableCmd>) {
291 self.commands.push(cmd);
292 }
293
294 pub fn push_executed(&mut self, cmd: Box<dyn UndoableCmd>) {
299 self.commands.push(cmd);
300 self.executed_to = self.commands.len();
301 }
302
303 #[must_use]
305 pub fn len(&self) -> usize {
306 self.commands.len()
307 }
308
309 #[must_use]
311 pub fn is_empty(&self) -> bool {
312 self.commands.is_empty()
313 }
314}
315
316impl UndoableCmd for CommandBatch {
317 fn execute(&mut self) -> CommandResult {
318 for (i, cmd) in self.commands.iter_mut().enumerate() {
319 if let Err(e) = cmd.execute() {
320 for j in (0..i).rev() {
322 let _ = self.commands[j].undo();
323 }
324 return Err(e);
325 }
326 self.executed_to = i + 1;
327 }
328 Ok(())
329 }
330
331 fn undo(&mut self) -> CommandResult {
332 for i in (0..self.executed_to).rev() {
334 self.commands[i].undo()?;
335 }
336 self.executed_to = 0;
337 Ok(())
338 }
339
340 fn redo(&mut self) -> CommandResult {
341 self.execute()
342 }
343
344 fn description(&self) -> &str {
345 &self.metadata.description
346 }
347
348 fn size_bytes(&self) -> usize {
349 std::mem::size_of::<Self>()
350 + self.metadata.size_bytes()
351 + self.commands.iter().map(|c| c.size_bytes()).sum::<usize>()
352 }
353
354 fn metadata(&self) -> &CommandMetadata {
355 &self.metadata
356 }
357
358 fn as_any(&self) -> &dyn Any {
359 self
360 }
361
362 fn as_any_mut(&mut self) -> &mut dyn Any {
363 self
364 }
365
366 fn debug_name(&self) -> &'static str {
367 "CommandBatch"
368 }
369}
370
371pub type TextApplyFn = Box<dyn Fn(WidgetId, usize, &str) -> CommandResult + Send + Sync>;
377pub type TextRemoveFn = Box<dyn Fn(WidgetId, usize, usize) -> CommandResult + Send + Sync>;
379pub type TextReplaceFn = Box<dyn Fn(WidgetId, usize, usize, &str) -> CommandResult + Send + Sync>;
381
382pub struct TextInsertCmd {
384 pub target: WidgetId,
386 pub position: usize,
388 pub text: String,
390 pub metadata: CommandMetadata,
392 apply: Option<TextApplyFn>,
394 remove: Option<TextRemoveFn>,
396}
397
398impl fmt::Debug for TextInsertCmd {
399 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400 f.debug_struct("TextInsertCmd")
401 .field("target", &self.target)
402 .field("position", &self.position)
403 .field("text", &self.text)
404 .field("metadata", &self.metadata)
405 .field("has_apply", &self.apply.is_some())
406 .field("has_remove", &self.remove.is_some())
407 .finish()
408 }
409}
410
411impl TextInsertCmd {
412 #[must_use]
414 pub fn new(target: WidgetId, position: usize, text: impl Into<String>) -> Self {
415 Self {
416 target,
417 position,
418 text: text.into(),
419 metadata: CommandMetadata::new("Insert text"),
420 apply: None,
421 remove: None,
422 }
423 }
424
425 pub fn with_apply<F>(mut self, f: F) -> Self
427 where
428 F: Fn(WidgetId, usize, &str) -> CommandResult + Send + Sync + 'static,
429 {
430 self.apply = Some(Box::new(f));
431 self
432 }
433
434 pub fn with_remove<F>(mut self, f: F) -> Self
436 where
437 F: Fn(WidgetId, usize, usize) -> CommandResult + Send + Sync + 'static,
438 {
439 self.remove = Some(Box::new(f));
440 self
441 }
442}
443
444impl UndoableCmd for TextInsertCmd {
445 fn execute(&mut self) -> CommandResult {
446 if let Some(ref apply) = self.apply {
447 apply(self.target, self.position, &self.text)
448 } else {
449 Err(CommandError::InvalidState(
450 "no apply callback set".to_string(),
451 ))
452 }
453 }
454
455 fn undo(&mut self) -> CommandResult {
456 if let Some(ref remove) = self.remove {
457 remove(self.target, self.position, self.text.len())
458 } else {
459 Err(CommandError::InvalidState(
460 "no remove callback set".to_string(),
461 ))
462 }
463 }
464
465 fn description(&self) -> &str {
466 &self.metadata.description
467 }
468
469 fn size_bytes(&self) -> usize {
470 std::mem::size_of::<Self>() + self.text.len() + self.metadata.size_bytes()
471 }
472
473 fn can_merge(&self, other: &dyn UndoableCmd, config: &MergeConfig) -> bool {
474 let Some(other) = other.as_any().downcast_ref::<Self>() else {
475 return false;
476 };
477
478 if self.target != other.target {
480 return false;
481 }
482
483 if other.position != self.position + self.text.len() {
485 return false;
486 }
487
488 let elapsed = other
490 .metadata
491 .timestamp
492 .duration_since(self.metadata.timestamp);
493 if elapsed.as_millis() > config.max_delay_ms as u128 {
494 return false;
495 }
496
497 if self.text.len() + other.text.len() > config.max_merged_size {
499 return false;
500 }
501
502 if !config.merge_across_words && self.text.ends_with(' ') {
504 return false;
505 }
506
507 true
508 }
509
510 fn merge_text(&self) -> Option<&str> {
511 Some(&self.text)
512 }
513
514 fn accept_merge(&mut self, other: &dyn UndoableCmd) -> bool {
515 if let Some(text) = other.merge_text() {
516 self.text.push_str(text);
517 true
518 } else {
519 false
520 }
521 }
522
523 fn metadata(&self) -> &CommandMetadata {
524 &self.metadata
525 }
526
527 fn target(&self) -> Option<WidgetId> {
528 Some(self.target)
529 }
530
531 fn as_any(&self) -> &dyn Any {
532 self
533 }
534
535 fn as_any_mut(&mut self) -> &mut dyn Any {
536 self
537 }
538
539 fn debug_name(&self) -> &'static str {
540 "TextInsertCmd"
541 }
542}
543
544pub struct TextDeleteCmd {
546 pub target: WidgetId,
548 pub position: usize,
550 pub deleted_text: String,
552 pub metadata: CommandMetadata,
554 remove: Option<TextRemoveFn>,
556 insert: Option<TextApplyFn>,
558}
559
560impl fmt::Debug for TextDeleteCmd {
561 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562 f.debug_struct("TextDeleteCmd")
563 .field("target", &self.target)
564 .field("position", &self.position)
565 .field("deleted_text", &self.deleted_text)
566 .field("metadata", &self.metadata)
567 .field("has_remove", &self.remove.is_some())
568 .field("has_insert", &self.insert.is_some())
569 .finish()
570 }
571}
572
573impl TextDeleteCmd {
574 #[must_use]
576 pub fn new(target: WidgetId, position: usize, deleted_text: impl Into<String>) -> Self {
577 Self {
578 target,
579 position,
580 deleted_text: deleted_text.into(),
581 metadata: CommandMetadata::new("Delete text"),
582 remove: None,
583 insert: None,
584 }
585 }
586
587 pub fn with_remove<F>(mut self, f: F) -> Self
589 where
590 F: Fn(WidgetId, usize, usize) -> CommandResult + Send + Sync + 'static,
591 {
592 self.remove = Some(Box::new(f));
593 self
594 }
595
596 pub fn with_insert<F>(mut self, f: F) -> Self
598 where
599 F: Fn(WidgetId, usize, &str) -> CommandResult + Send + Sync + 'static,
600 {
601 self.insert = Some(Box::new(f));
602 self
603 }
604}
605
606impl UndoableCmd for TextDeleteCmd {
607 fn execute(&mut self) -> CommandResult {
608 if let Some(ref remove) = self.remove {
609 remove(self.target, self.position, self.deleted_text.len())
610 } else {
611 Err(CommandError::InvalidState(
612 "no remove callback set".to_string(),
613 ))
614 }
615 }
616
617 fn undo(&mut self) -> CommandResult {
618 if let Some(ref insert) = self.insert {
619 insert(self.target, self.position, &self.deleted_text)
620 } else {
621 Err(CommandError::InvalidState(
622 "no insert callback set".to_string(),
623 ))
624 }
625 }
626
627 fn description(&self) -> &str {
628 &self.metadata.description
629 }
630
631 fn size_bytes(&self) -> usize {
632 std::mem::size_of::<Self>() + self.deleted_text.len() + self.metadata.size_bytes()
633 }
634
635 fn can_merge(&self, other: &dyn UndoableCmd, config: &MergeConfig) -> bool {
636 let Some(other) = other.as_any().downcast_ref::<Self>() else {
637 return false;
638 };
639
640 if self.target != other.target {
642 return false;
643 }
644
645 let is_backspace = other.position + other.deleted_text.len() == self.position;
648 let is_delete = other.position == self.position;
649
650 if !is_backspace && !is_delete {
651 return false;
652 }
653
654 let elapsed = other
656 .metadata
657 .timestamp
658 .duration_since(self.metadata.timestamp);
659 if elapsed.as_millis() > config.max_delay_ms as u128 {
660 return false;
661 }
662
663 if self.deleted_text.len() + other.deleted_text.len() > config.max_merged_size {
665 return false;
666 }
667
668 true
669 }
670
671 fn merge_text(&self) -> Option<&str> {
672 Some(&self.deleted_text)
673 }
674
675 fn accept_merge(&mut self, other: &dyn UndoableCmd) -> bool {
676 let Some(text) = other.merge_text() else {
677 return false;
678 };
679 let Some(other_delete) = other.as_any().downcast_ref::<Self>() else {
680 return false;
681 };
682
683 let is_backspace = other_delete.position + other_delete.deleted_text.len() == self.position;
689
690 if is_backspace {
691 self.deleted_text = format!("{}{}", text, self.deleted_text);
693 self.position = other_delete.position;
694 } else {
695 self.deleted_text.push_str(text);
697 }
698 true
699 }
700
701 fn metadata(&self) -> &CommandMetadata {
702 &self.metadata
703 }
704
705 fn target(&self) -> Option<WidgetId> {
706 Some(self.target)
707 }
708
709 fn as_any(&self) -> &dyn Any {
710 self
711 }
712
713 fn as_any_mut(&mut self) -> &mut dyn Any {
714 self
715 }
716
717 fn debug_name(&self) -> &'static str {
718 "TextDeleteCmd"
719 }
720}
721
722pub struct TextReplaceCmd {
724 pub target: WidgetId,
726 pub position: usize,
728 pub old_text: String,
730 pub new_text: String,
732 pub metadata: CommandMetadata,
734 replace: Option<TextReplaceFn>,
736}
737
738impl fmt::Debug for TextReplaceCmd {
739 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
740 f.debug_struct("TextReplaceCmd")
741 .field("target", &self.target)
742 .field("position", &self.position)
743 .field("old_text", &self.old_text)
744 .field("new_text", &self.new_text)
745 .field("metadata", &self.metadata)
746 .field("has_replace", &self.replace.is_some())
747 .finish()
748 }
749}
750
751impl TextReplaceCmd {
752 #[must_use]
754 pub fn new(
755 target: WidgetId,
756 position: usize,
757 old_text: impl Into<String>,
758 new_text: impl Into<String>,
759 ) -> Self {
760 Self {
761 target,
762 position,
763 old_text: old_text.into(),
764 new_text: new_text.into(),
765 metadata: CommandMetadata::new("Replace text"),
766 replace: None,
767 }
768 }
769
770 pub fn with_replace<F>(mut self, f: F) -> Self
772 where
773 F: Fn(WidgetId, usize, usize, &str) -> CommandResult + Send + Sync + 'static,
774 {
775 self.replace = Some(Box::new(f));
776 self
777 }
778}
779
780impl UndoableCmd for TextReplaceCmd {
781 fn execute(&mut self) -> CommandResult {
782 if let Some(ref replace) = self.replace {
783 replace(
784 self.target,
785 self.position,
786 self.old_text.len(),
787 &self.new_text,
788 )
789 } else {
790 Err(CommandError::InvalidState(
791 "no replace callback set".to_string(),
792 ))
793 }
794 }
795
796 fn undo(&mut self) -> CommandResult {
797 if let Some(ref replace) = self.replace {
798 replace(
799 self.target,
800 self.position,
801 self.new_text.len(),
802 &self.old_text,
803 )
804 } else {
805 Err(CommandError::InvalidState(
806 "no replace callback set".to_string(),
807 ))
808 }
809 }
810
811 fn description(&self) -> &str {
812 &self.metadata.description
813 }
814
815 fn size_bytes(&self) -> usize {
816 std::mem::size_of::<Self>()
817 + self.old_text.len()
818 + self.new_text.len()
819 + self.metadata.size_bytes()
820 }
821
822 fn metadata(&self) -> &CommandMetadata {
823 &self.metadata
824 }
825
826 fn target(&self) -> Option<WidgetId> {
827 Some(self.target)
828 }
829
830 fn as_any(&self) -> &dyn Any {
831 self
832 }
833
834 fn as_any_mut(&mut self) -> &mut dyn Any {
835 self
836 }
837
838 fn debug_name(&self) -> &'static str {
839 "TextReplaceCmd"
840 }
841}
842
843#[cfg(test)]
848mod tests {
849 use super::*;
850 use std::sync::Arc;
851 use std::sync::Mutex;
852
853 #[test]
854 fn test_widget_id_creation() {
855 let id = WidgetId::new(42);
856 assert_eq!(id.raw(), 42);
857 }
858
859 #[test]
860 fn test_command_metadata_size() {
861 let meta = CommandMetadata::new("Test command");
862 let size = meta.size_bytes();
863 assert!(size > std::mem::size_of::<CommandMetadata>());
864 assert!(size >= std::mem::size_of::<CommandMetadata>() + "Test command".len());
865 }
866
867 #[test]
868 fn test_command_metadata_with_source() {
869 let meta = CommandMetadata::new("Test").with_source(CommandSource::Macro);
870 assert_eq!(meta.source, CommandSource::Macro);
871 }
872
873 #[test]
874 fn test_command_metadata_with_batch() {
875 let meta = CommandMetadata::new("Test").with_batch(123);
876 assert_eq!(meta.batch_id, Some(123));
877 }
878
879 #[test]
880 fn test_command_batch_execute_undo() {
881 let buffer = Arc::new(Mutex::new(String::new()));
883
884 let mut batch = CommandBatch::new("Test batch");
885
886 let b1 = buffer.clone();
888 let b2 = buffer.clone();
889 let b3 = buffer.clone();
890 let b4 = buffer.clone();
891
892 let cmd1 = TextInsertCmd::new(WidgetId::new(1), 0, "Hello")
893 .with_apply(move |_, pos, text| {
894 let mut buf = b1.lock().unwrap();
895 buf.insert_str(pos, text);
896 Ok(())
897 })
898 .with_remove(move |_, pos, len| {
899 let mut buf = b2.lock().unwrap();
900 buf.drain(pos..pos + len);
901 Ok(())
902 });
903
904 let cmd2 = TextInsertCmd::new(WidgetId::new(1), 5, " World")
905 .with_apply(move |_, pos, text| {
906 let mut buf = b3.lock().unwrap();
907 buf.insert_str(pos, text);
908 Ok(())
909 })
910 .with_remove(move |_, pos, len| {
911 let mut buf = b4.lock().unwrap();
912 buf.drain(pos..pos + len);
913 Ok(())
914 });
915
916 batch.push(Box::new(cmd1));
917 batch.push(Box::new(cmd2));
918
919 batch.execute().unwrap();
921 assert_eq!(*buffer.lock().unwrap(), "Hello World");
922
923 batch.undo().unwrap();
925 assert_eq!(*buffer.lock().unwrap(), "");
926 }
927
928 #[test]
929 fn test_command_batch_empty() {
930 let batch = CommandBatch::new("Empty");
931 assert!(batch.is_empty());
932 assert_eq!(batch.len(), 0);
933 }
934
935 #[test]
936 fn test_text_insert_can_merge_consecutive() {
937 let cmd1 = TextInsertCmd::new(WidgetId::new(1), 0, "a");
938 let mut cmd2 = TextInsertCmd::new(WidgetId::new(1), 1, "b");
939 cmd2.metadata.timestamp = cmd1.metadata.timestamp;
941
942 let config = MergeConfig::default();
943 assert!(cmd1.can_merge(&cmd2, &config));
944 }
945
946 #[test]
947 fn test_text_insert_no_merge_different_widget() {
948 let cmd1 = TextInsertCmd::new(WidgetId::new(1), 0, "a");
949 let mut cmd2 = TextInsertCmd::new(WidgetId::new(2), 1, "b");
950 cmd2.metadata.timestamp = cmd1.metadata.timestamp;
951
952 let config = MergeConfig::default();
953 assert!(!cmd1.can_merge(&cmd2, &config));
954 }
955
956 #[test]
957 fn test_text_insert_no_merge_non_consecutive() {
958 let cmd1 = TextInsertCmd::new(WidgetId::new(1), 0, "a");
959 let mut cmd2 = TextInsertCmd::new(WidgetId::new(1), 5, "b");
960 cmd2.metadata.timestamp = cmd1.metadata.timestamp;
961
962 let config = MergeConfig::default();
963 assert!(!cmd1.can_merge(&cmd2, &config));
964 }
965
966 #[test]
967 fn test_text_delete_can_merge_backspace() {
968 let cmd1 = TextDeleteCmd::new(WidgetId::new(1), 5, "b");
969 let mut cmd2 = TextDeleteCmd::new(WidgetId::new(1), 4, "a");
970 cmd2.metadata.timestamp = cmd1.metadata.timestamp;
971
972 let config = MergeConfig::default();
973 assert!(cmd1.can_merge(&cmd2, &config));
974 }
975
976 #[test]
977 fn test_text_delete_can_merge_delete_key() {
978 let cmd1 = TextDeleteCmd::new(WidgetId::new(1), 5, "a");
979 let mut cmd2 = TextDeleteCmd::new(WidgetId::new(1), 5, "b");
980 cmd2.metadata.timestamp = cmd1.metadata.timestamp;
981
982 let config = MergeConfig::default();
983 assert!(cmd1.can_merge(&cmd2, &config));
984 }
985
986 #[test]
987 fn test_command_error_display() {
988 let err = CommandError::TargetNotFound(WidgetId::new(42));
989 assert!(err.to_string().contains("42"));
990
991 let err = CommandError::PositionOutOfBounds {
992 position: 10,
993 length: 5,
994 };
995 assert!(err.to_string().contains("10"));
996 assert!(err.to_string().contains("5"));
997 }
998
999 #[test]
1000 fn test_merge_config_default() {
1001 let config = MergeConfig::default();
1002 assert_eq!(config.max_delay_ms, 500);
1003 assert!(!config.merge_across_words);
1004 assert_eq!(config.max_merged_size, 1024);
1005 }
1006
1007 #[test]
1008 fn test_text_replace_size_bytes() {
1009 let cmd = TextReplaceCmd::new(WidgetId::new(1), 0, "old", "new");
1010 let size = cmd.size_bytes();
1011 assert!(size >= std::mem::size_of::<TextReplaceCmd>() + 3 + 3);
1012 }
1013
1014 #[test]
1015 fn test_text_insert_accept_merge() {
1016 let mut cmd1 = TextInsertCmd::new(WidgetId::new(1), 0, "Hello");
1017 let cmd2 = TextInsertCmd::new(WidgetId::new(1), 5, " World");
1018 assert!(cmd1.accept_merge(&cmd2));
1019 assert_eq!(cmd1.text, "Hello World");
1020 }
1021
1022 #[test]
1023 fn test_text_delete_accept_merge_backspace() {
1024 let mut cmd1 = TextDeleteCmd::new(WidgetId::new(1), 4, "b");
1028 let cmd2 = TextDeleteCmd::new(WidgetId::new(1), 3, "a");
1029 assert!(cmd1.accept_merge(&cmd2));
1030 assert_eq!(cmd1.deleted_text, "ab");
1031 assert_eq!(cmd1.position, 3); }
1033
1034 #[test]
1035 fn test_text_delete_accept_merge_forward_delete() {
1036 let mut cmd1 = TextDeleteCmd::new(WidgetId::new(1), 3, "a");
1040 let cmd2 = TextDeleteCmd::new(WidgetId::new(1), 3, "b");
1041 assert!(cmd1.accept_merge(&cmd2));
1042 assert_eq!(cmd1.deleted_text, "ab");
1043 assert_eq!(cmd1.position, 3); }
1045
1046 #[test]
1047 fn test_debug_implementations() {
1048 let cmd = TextInsertCmd::new(WidgetId::new(1), 0, "test");
1049 let debug_str = format!("{:?}", cmd);
1050 assert!(debug_str.contains("TextInsertCmd"));
1051 assert!(debug_str.contains("test"));
1052
1053 let batch = CommandBatch::new("Test batch");
1054 let debug_str = format!("{:?}", batch);
1055 assert!(debug_str.contains("CommandBatch"));
1056 }
1057}