1use crate::model::buffer::BufferSnapshot;
2pub use fresh_core::api::{OverlayColorSpec, OverlayOptions};
3pub use fresh_core::overlay::{OverlayHandle, OverlayNamespace};
4pub use fresh_core::{BufferId, ContainerId, CursorId, LeafId, SplitDirection, SplitId};
5use serde::{Deserialize, Serialize};
6use std::ops::Range;
7use std::sync::Arc;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub enum Event {
12 Insert {
14 position: usize,
15 text: String,
16 cursor_id: CursorId,
17 },
18
19 Delete {
21 range: Range<usize>,
22 deleted_text: String,
23 cursor_id: CursorId,
24 },
25
26 MoveCursor {
28 cursor_id: CursorId,
29 old_position: usize,
30 new_position: usize,
31 old_anchor: Option<usize>,
32 new_anchor: Option<usize>,
33 old_sticky_column: usize,
34 new_sticky_column: usize,
35 },
36
37 AddCursor {
39 cursor_id: CursorId,
40 position: usize,
41 anchor: Option<usize>,
42 },
43
44 RemoveCursor {
46 cursor_id: CursorId,
47 position: usize,
48 anchor: Option<usize>,
49 },
50
51 Scroll {
53 line_offset: isize,
54 },
55
56 SetViewport {
58 top_line: usize,
59 },
60
61 Recenter,
63
64 SetAnchor {
66 cursor_id: CursorId,
67 position: usize,
68 },
69
70 ClearAnchor {
73 cursor_id: CursorId,
74 },
75
76 ChangeMode {
78 mode: String,
79 },
80
81 AddOverlay {
83 namespace: Option<OverlayNamespace>,
84 range: Range<usize>,
85 face: OverlayFace,
86 priority: i32,
87 message: Option<String>,
88 extend_to_line_end: bool,
90 url: Option<String>,
92 },
93
94 RemoveOverlay {
96 handle: OverlayHandle,
97 },
98
99 RemoveOverlaysInRange {
101 range: Range<usize>,
102 },
103
104 ClearNamespace {
106 namespace: OverlayNamespace,
107 },
108
109 ClearOverlays,
111
112 ShowPopup {
114 popup: PopupData,
115 },
116
117 HidePopup,
119
120 ClearPopups,
122
123 PopupSelectNext,
125 PopupSelectPrev,
126 PopupPageDown,
127 PopupPageUp,
128
129 AddMarginAnnotation {
132 line: usize,
133 position: MarginPositionData,
134 content: MarginContentData,
135 annotation_id: Option<String>,
136 },
137
138 RemoveMarginAnnotation {
140 annotation_id: String,
141 },
142
143 RemoveMarginAnnotationsAtLine {
145 line: usize,
146 position: MarginPositionData,
147 },
148
149 ClearMarginPosition {
151 position: MarginPositionData,
152 },
153
154 ClearMargins,
156
157 SetLineNumbers {
159 enabled: bool,
160 },
161
162 SplitPane {
165 direction: SplitDirection,
166 new_buffer_id: BufferId,
167 ratio: f32,
168 },
169
170 CloseSplit {
172 split_id: SplitId,
173 },
174
175 SetActiveSplit {
177 split_id: SplitId,
178 },
179
180 AdjustSplitRatio {
182 split_id: SplitId,
183 delta: f32,
184 },
185
186 NextSplit,
188
189 PrevSplit,
191
192 Batch {
195 events: Vec<Event>,
196 description: String,
197 },
198
199 BulkEdit {
206 #[serde(skip)]
208 old_snapshot: Option<Arc<BufferSnapshot>>,
209 #[serde(skip)]
211 new_snapshot: Option<Arc<BufferSnapshot>>,
212 old_cursors: Vec<(CursorId, usize, Option<usize>)>,
214 new_cursors: Vec<(CursorId, usize, Option<usize>)>,
216 description: String,
218 #[serde(default)]
223 edits: Vec<(usize, usize, usize)>,
224 #[serde(default)]
229 displaced_markers: Vec<(u64, usize)>,
230 },
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub enum OverlayFace {
236 Underline {
237 color: (u8, u8, u8), style: UnderlineStyle,
239 },
240 Background {
241 color: (u8, u8, u8),
242 },
243 Foreground {
244 color: (u8, u8, u8),
245 },
246 Style {
251 options: OverlayOptions,
252 },
253}
254
255impl OverlayFace {
256 pub fn from_options(options: OverlayOptions) -> Self {
258 OverlayFace::Style { options }
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
264pub enum UnderlineStyle {
265 Straight,
266 Wavy,
267 Dotted,
268 Dashed,
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
276pub enum PopupKindHint {
277 Completion,
279 #[default]
281 List,
282 Text,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct PopupData {
289 #[serde(default)]
291 pub kind: PopupKindHint,
292 pub title: Option<String>,
293 #[serde(default)]
295 pub description: Option<String>,
296 #[serde(default)]
297 pub transient: bool,
298 pub content: PopupContentData,
299 pub position: PopupPositionData,
300 pub width: u16,
301 pub max_height: u16,
302 pub bordered: bool,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307pub enum PopupContentData {
308 Text(Vec<String>),
309 List {
310 items: Vec<PopupListItemData>,
311 selected: usize,
312 },
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct PopupListItemData {
318 pub text: String,
319 pub detail: Option<String>,
320 pub icon: Option<String>,
321 pub data: Option<String>,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub enum PopupPositionData {
327 AtCursor,
328 BelowCursor,
329 AboveCursor,
330 Fixed { x: u16, y: u16 },
331 Centered,
332 BottomRight,
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
337pub enum MarginPositionData {
338 Left,
339 Right,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub enum MarginContentData {
345 Text(String),
346 Symbol {
347 text: String,
348 color: Option<(u8, u8, u8)>, },
350 Empty,
351}
352
353impl Event {
354 pub fn inverse(&self) -> Option<Self> {
357 match self {
358 Self::Insert { position, text, .. } => {
359 let range = *position..(position + text.len());
360 Some(Self::Delete {
361 range,
362 deleted_text: text.clone(),
363 cursor_id: CursorId::UNDO_SENTINEL,
364 })
365 }
366 Self::Delete {
367 range,
368 deleted_text,
369 ..
370 } => Some(Self::Insert {
371 position: range.start,
372 text: deleted_text.clone(),
373 cursor_id: CursorId::UNDO_SENTINEL,
374 }),
375 Self::Batch {
376 events,
377 description,
378 } => {
379 let inverted: Option<Vec<Self>> =
381 events.iter().rev().map(|e| e.inverse()).collect();
382
383 inverted.map(|inverted_events| Self::Batch {
384 events: inverted_events,
385 description: format!("Undo: {}", description),
386 })
387 }
388 Self::AddCursor {
389 cursor_id,
390 position,
391 anchor,
392 } => {
393 Some(Self::RemoveCursor {
395 cursor_id: *cursor_id,
396 position: *position,
397 anchor: *anchor,
398 })
399 }
400 Self::RemoveCursor {
401 cursor_id,
402 position,
403 anchor,
404 } => {
405 Some(Self::AddCursor {
407 cursor_id: *cursor_id,
408 position: *position,
409 anchor: *anchor,
410 })
411 }
412 Self::MoveCursor {
413 cursor_id,
414 old_position,
415 new_position,
416 old_anchor,
417 new_anchor,
418 old_sticky_column,
419 new_sticky_column,
420 } => {
421 Some(Self::MoveCursor {
423 cursor_id: *cursor_id,
424 old_position: *new_position,
425 new_position: *old_position,
426 old_anchor: *new_anchor,
427 new_anchor: *old_anchor,
428 old_sticky_column: *new_sticky_column,
429 new_sticky_column: *old_sticky_column,
430 })
431 }
432 Self::AddOverlay { .. } => {
433 None
435 }
436 Self::RemoveOverlay { .. } => {
437 None
439 }
440 Self::ClearNamespace { .. } => {
441 None
443 }
444 Self::Scroll { line_offset } => Some(Self::Scroll {
445 line_offset: -line_offset,
446 }),
447 Self::SetViewport { top_line: _ } => {
448 None
450 }
451 Self::ChangeMode { mode: _ } => {
452 None
454 }
455 Self::BulkEdit {
456 old_snapshot,
457 new_snapshot,
458 old_cursors,
459 new_cursors,
460 description,
461 edits,
462 displaced_markers,
463 } => {
464 let inverted_edits: Vec<(usize, usize, usize)> = edits
467 .iter()
468 .map(|(pos, del_len, ins_len)| (*pos, *ins_len, *del_len))
469 .collect();
470
471 Some(Self::BulkEdit {
472 old_snapshot: new_snapshot.clone(),
473 new_snapshot: old_snapshot.clone(),
474 old_cursors: new_cursors.clone(),
475 new_cursors: old_cursors.clone(),
476 description: format!("Undo: {}", description),
477 edits: inverted_edits,
478 displaced_markers: displaced_markers.clone(),
482 })
483 }
484 _ => None,
486 }
487 }
488
489 pub fn modifies_buffer(&self) -> bool {
491 match self {
492 Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
493 Self::Batch { events, .. } => events.iter().any(|e| e.modifies_buffer()),
494 _ => false,
495 }
496 }
497
498 pub fn is_write_action(&self) -> bool {
511 match self {
512 Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
514
515 Self::AddCursor { .. } | Self::RemoveCursor { .. } => true,
517
518 Self::Batch { events, .. } => events.iter().any(|e| e.is_write_action()),
520
521 _ => false,
523 }
524 }
525
526 pub fn cursor_id(&self) -> Option<CursorId> {
528 match self {
529 Self::Insert { cursor_id, .. }
530 | Self::Delete { cursor_id, .. }
531 | Self::MoveCursor { cursor_id, .. }
532 | Self::AddCursor { cursor_id, .. }
533 | Self::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
534 _ => None,
535 }
536 }
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct LogEntry {
542 pub event: Event,
544
545 pub timestamp: u64,
547
548 pub description: Option<String>,
550
551 #[serde(default, skip_serializing_if = "Vec::is_empty")]
556 pub displaced_markers: Vec<(u64, usize)>,
557}
558
559impl LogEntry {
560 pub fn new(event: Event) -> Self {
561 Self {
562 event,
563 timestamp: std::time::SystemTime::now()
564 .duration_since(std::time::UNIX_EPOCH)
565 .unwrap()
566 .as_millis() as u64,
567 description: None,
568 displaced_markers: Vec::new(),
569 }
570 }
571
572 pub fn with_description(mut self, description: String) -> Self {
573 self.description = Some(description);
574 self
575 }
576}
577
578#[derive(Debug, Clone)]
580pub struct Snapshot {
581 pub log_index: usize,
583
584 pub buffer_state: (),
587
588 pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
590}
591
592pub struct EventLog {
594 entries: Vec<LogEntry>,
596
597 current_index: usize,
599
600 snapshots: Vec<Snapshot>,
602
603 snapshot_interval: usize,
605
606 #[cfg(feature = "runtime")]
608 stream_file: Option<std::fs::File>,
609
610 saved_at_index: Option<usize>,
613}
614
615impl EventLog {
616 pub fn new() -> Self {
618 Self {
619 entries: Vec::new(),
620 current_index: 0,
621 snapshots: Vec::new(),
622 snapshot_interval: 100,
623 #[cfg(feature = "runtime")]
624 stream_file: None,
625 saved_at_index: Some(0), }
627 }
628
629 pub fn mark_saved(&mut self) {
632 self.saved_at_index = Some(self.current_index);
633 }
634
635 pub fn clear_saved_position(&mut self) {
639 self.saved_at_index = None;
640 }
641
642 pub fn is_at_saved_position(&self) -> bool {
646 match self.saved_at_index {
647 None => false,
648 Some(saved_idx) if saved_idx == self.current_index => true,
649 Some(saved_idx) => {
650 let (start, end) = if saved_idx < self.current_index {
653 (saved_idx, self.current_index)
654 } else {
655 (self.current_index, saved_idx)
656 };
657
658 self.entries[start..end]
660 .iter()
661 .all(|entry| !entry.event.modifies_buffer())
662 }
663 }
664 }
665
666 #[cfg(feature = "runtime")]
668 pub fn enable_streaming<P: AsRef<std::path::Path>>(&mut self, path: P) -> std::io::Result<()> {
669 use std::io::Write;
670
671 let mut file = std::fs::OpenOptions::new()
672 .create(true)
673 .write(true)
674 .truncate(true)
675 .open(path)?;
676
677 writeln!(file, "# Event Log Stream")?;
679 writeln!(file, "# Started at: {}", chrono::Local::now())?;
680 writeln!(file, "# Format: JSON Lines (one event per line)")?;
681 writeln!(file, "#")?;
682
683 self.stream_file = Some(file);
684 Ok(())
685 }
686
687 #[cfg(feature = "runtime")]
689 pub fn disable_streaming(&mut self) {
690 self.stream_file = None;
691 }
692
693 #[cfg(feature = "runtime")]
695 pub fn log_render_state(
696 &mut self,
697 cursor_pos: usize,
698 screen_cursor_x: u16,
699 screen_cursor_y: u16,
700 buffer_len: usize,
701 ) {
702 if let Some(ref mut file) = self.stream_file {
703 use std::io::Write;
704
705 let render_info = serde_json::json!({
706 "type": "render",
707 "timestamp": chrono::Local::now().to_rfc3339(),
708 "cursor_position": cursor_pos,
709 "screen_cursor": {"x": screen_cursor_x, "y": screen_cursor_y},
710 "buffer_length": buffer_len,
711 });
712
713 if let Err(e) = writeln!(file, "{render_info}") {
714 tracing::trace!("Warning: Failed to write render info to stream: {e}");
715 }
716 if let Err(e) = file.flush() {
717 tracing::trace!("Warning: Failed to flush event stream: {e}");
718 }
719 }
720 }
721
722 #[cfg(feature = "runtime")]
724 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
725 if let Some(ref mut file) = self.stream_file {
726 use std::io::Write;
727
728 let keystroke_info = serde_json::json!({
729 "type": "keystroke",
730 "timestamp": chrono::Local::now().to_rfc3339(),
731 "key": key_code,
732 "modifiers": modifiers,
733 });
734
735 if let Err(e) = writeln!(file, "{keystroke_info}") {
736 tracing::trace!("Warning: Failed to write keystroke to stream: {e}");
737 }
738 if let Err(e) = file.flush() {
739 tracing::trace!("Warning: Failed to flush event stream: {e}");
740 }
741 }
742 }
743
744 pub fn append(&mut self, event: Event) -> usize {
746 if self.current_index < self.entries.len() {
752 if event.is_write_action() {
753 self.entries.truncate(self.current_index);
755
756 if let Some(saved_idx) = self.saved_at_index {
758 if saved_idx > self.current_index {
759 self.saved_at_index = None;
760 }
761 }
762 } else {
763 return self.current_index;
765 }
766 }
767
768 #[cfg(feature = "runtime")]
770 if let Some(ref mut file) = self.stream_file {
771 use std::io::Write;
772
773 let stream_entry = serde_json::json!({
774 "index": self.entries.len(),
775 "timestamp": chrono::Local::now().to_rfc3339(),
776 "event": event,
777 });
778
779 if let Err(e) = writeln!(file, "{stream_entry}") {
781 tracing::trace!("Warning: Failed to write to event stream: {e}");
782 }
783 if let Err(e) = file.flush() {
784 tracing::trace!("Warning: Failed to flush event stream: {e}");
785 }
786 }
787
788 let entry = LogEntry::new(event);
789 self.entries.push(entry);
790 self.current_index = self.entries.len();
791
792 if self.entries.len().is_multiple_of(self.snapshot_interval) {
794 }
797
798 self.current_index - 1
799 }
800
801 pub fn set_displaced_markers_on_last(&mut self, markers: Vec<(u64, usize)>) {
805 if let Some(entry) = self.entries.last_mut() {
806 entry.displaced_markers = markers;
807 }
808 }
809
810 pub fn current_index(&self) -> usize {
812 self.current_index
813 }
814
815 pub fn len(&self) -> usize {
817 self.entries.len()
818 }
819
820 pub fn is_empty(&self) -> bool {
822 self.entries.is_empty()
823 }
824
825 pub fn can_undo(&self) -> bool {
827 self.current_index > 0
828 }
829
830 pub fn can_redo(&self) -> bool {
832 self.current_index < self.entries.len()
833 }
834
835 pub fn undo(&mut self) -> Vec<(Event, Vec<(u64, usize)>)> {
841 let mut inverse_events = Vec::new();
842 let mut found_write_action = false;
843
844 while self.can_undo() && !found_write_action {
846 self.current_index -= 1;
847 let entry = &self.entries[self.current_index];
848
849 if entry.event.is_write_action() {
851 found_write_action = true;
852 }
853
854 if let Some(inverse) = entry.event.inverse() {
856 inverse_events.push((inverse, entry.displaced_markers.clone()));
857 }
858 }
860
861 inverse_events
862 }
863
864 pub fn redo(&mut self) -> Vec<Event> {
868 let mut events = Vec::new();
869 let mut found_write_action = false;
870
871 while self.can_redo() {
873 let event = self.entries[self.current_index].event.clone();
874
875 if found_write_action && event.is_write_action() {
877 break;
879 }
880
881 self.current_index += 1;
882
883 if event.is_write_action() {
885 found_write_action = true;
886 }
887
888 events.push(event);
889 }
890
891 events
892 }
893
894 pub fn entries(&self) -> &[LogEntry] {
896 &self.entries
897 }
898
899 pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
901 &self.entries[range]
902 }
903
904 pub fn last_event(&self) -> Option<&Event> {
906 if self.current_index > 0 {
907 Some(&self.entries[self.current_index - 1].event)
908 } else {
909 None
910 }
911 }
912
913 pub fn clear(&mut self) {
915 self.entries.clear();
916 self.current_index = 0;
917 self.snapshots.clear();
918 }
919
920 pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
922 use std::io::Write;
923 let file = std::fs::File::create(path)?;
924 let mut writer = std::io::BufWriter::new(file);
925
926 for entry in &self.entries {
927 let json = serde_json::to_string(entry)?;
928 writeln!(writer, "{json}")?;
929 }
930
931 Ok(())
932 }
933
934 pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
936 use std::io::BufRead;
937 let file = std::fs::File::open(path)?;
938 let reader = std::io::BufReader::new(file);
939
940 let mut log = Self::new();
941
942 for line in reader.lines() {
943 let line = line?;
944 if line.trim().is_empty() {
945 continue;
946 }
947 let entry: LogEntry = serde_json::from_str(&line)?;
948 log.entries.push(entry);
949 }
950
951 log.current_index = log.entries.len();
952
953 Ok(log)
954 }
955
956 pub fn set_snapshot_interval(&mut self, interval: usize) {
958 self.snapshot_interval = interval;
959 }
960}
961
962impl Default for EventLog {
963 fn default() -> Self {
964 Self::new()
965 }
966}
967
968#[cfg(test)]
969mod tests {
970 use super::*;
971
972 #[cfg(test)]
974 mod property_tests {
975 use super::*;
976 use proptest::prelude::*;
977
978 fn arb_event() -> impl Strategy<Value = Event> {
980 prop_oneof![
981 (0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
983 position: pos,
984 text,
985 cursor_id: CursorId(0),
986 }),
987 (0usize..1000, 1usize..50).prop_map(|(pos, len)| Event::Delete {
989 range: pos..pos + len,
990 deleted_text: "x".repeat(len),
991 cursor_id: CursorId(0),
992 }),
993 ]
994 }
995
996 proptest! {
997 #[test]
999 fn event_inverse_property(event in arb_event()) {
1000 if let Some(inverse) = event.inverse() {
1001 if let Some(double_inverse) = inverse.inverse() {
1004 match (&event, &double_inverse) {
1005 (Event::Insert { position: p1, text: t1, .. },
1006 Event::Insert { position: p2, text: t2, .. }) => {
1007 assert_eq!(p1, p2);
1008 assert_eq!(t1, t2);
1009 }
1010 (Event::Delete { range: r1, deleted_text: dt1, .. },
1011 Event::Delete { range: r2, deleted_text: dt2, .. }) => {
1012 assert_eq!(r1, r2);
1013 assert_eq!(dt1, dt2);
1014 }
1015 _ => {}
1016 }
1017 }
1018 }
1019 }
1020
1021 #[test]
1023 fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
1024 let mut log = EventLog::new();
1025
1026 for event in &events {
1028 log.append(event.clone());
1029 }
1030
1031 let after_append = log.current_index();
1032
1033 let mut undo_count = 0;
1035 while log.can_undo() {
1036 log.undo();
1037 undo_count += 1;
1038 }
1039
1040 assert_eq!(log.current_index(), 0);
1041 assert_eq!(undo_count, events.len());
1042
1043 let mut redo_count = 0;
1045 while log.can_redo() {
1046 log.redo();
1047 redo_count += 1;
1048 }
1049
1050 assert_eq!(log.current_index(), after_append);
1051 assert_eq!(redo_count, events.len());
1052 }
1053
1054 #[test]
1056 fn append_after_undo_truncates(
1057 initial_events in prop::collection::vec(arb_event(), 2..10),
1058 new_event in arb_event()
1059 ) {
1060 let mut log = EventLog::new();
1061
1062 for event in &initial_events {
1063 log.append(event.clone());
1064 }
1065
1066 log.undo();
1068 let index_after_undo = log.current_index();
1069
1070 log.append(new_event);
1072
1073 assert_eq!(log.current_index(), index_after_undo + 1);
1075 assert!(!log.can_redo());
1076 }
1077 }
1078 }
1079
1080 #[test]
1081 fn test_event_log_append() {
1082 let mut log = EventLog::new();
1083 let event = Event::Insert {
1084 position: 0,
1085 text: "hello".to_string(),
1086 cursor_id: CursorId(0),
1087 };
1088
1089 let index = log.append(event);
1090 assert_eq!(index, 0);
1091 assert_eq!(log.current_index(), 1);
1092 assert_eq!(log.entries().len(), 1);
1093 }
1094
1095 #[test]
1096 fn test_undo_redo() {
1097 let mut log = EventLog::new();
1098
1099 log.append(Event::Insert {
1100 position: 0,
1101 text: "a".to_string(),
1102 cursor_id: CursorId(0),
1103 });
1104
1105 log.append(Event::Insert {
1106 position: 1,
1107 text: "b".to_string(),
1108 cursor_id: CursorId(0),
1109 });
1110
1111 assert_eq!(log.current_index(), 2);
1112 assert!(log.can_undo());
1113 assert!(!log.can_redo());
1114
1115 log.undo();
1116 assert_eq!(log.current_index(), 1);
1117 assert!(log.can_undo());
1118 assert!(log.can_redo());
1119
1120 log.undo();
1121 assert_eq!(log.current_index(), 0);
1122 assert!(!log.can_undo());
1123 assert!(log.can_redo());
1124
1125 log.redo();
1126 assert_eq!(log.current_index(), 1);
1127 }
1128
1129 #[test]
1130 fn test_event_inverse() {
1131 let insert = Event::Insert {
1132 position: 5,
1133 text: "hello".to_string(),
1134 cursor_id: CursorId(0),
1135 };
1136
1137 let inverse = insert.inverse().unwrap();
1138 match inverse {
1139 Event::Delete {
1140 range,
1141 deleted_text,
1142 ..
1143 } => {
1144 assert_eq!(range, 5..10);
1145 assert_eq!(deleted_text, "hello");
1146 }
1147 _ => panic!("Expected Delete event"),
1148 }
1149 }
1150
1151 #[test]
1152 fn test_truncate_on_new_event_after_undo() {
1153 let mut log = EventLog::new();
1154
1155 log.append(Event::Insert {
1156 position: 0,
1157 text: "a".to_string(),
1158 cursor_id: CursorId(0),
1159 });
1160
1161 log.append(Event::Insert {
1162 position: 1,
1163 text: "b".to_string(),
1164 cursor_id: CursorId(0),
1165 });
1166
1167 log.undo();
1168 assert_eq!(log.entries().len(), 2);
1169
1170 log.append(Event::Insert {
1172 position: 1,
1173 text: "c".to_string(),
1174 cursor_id: CursorId(0),
1175 });
1176
1177 assert_eq!(log.entries().len(), 2);
1178 assert_eq!(log.current_index(), 2);
1179 }
1180
1181 #[test]
1182 fn test_navigation_after_undo_preserves_redo() {
1183 let mut log = EventLog::new();
1186
1187 log.append(Event::Insert {
1189 position: 0,
1190 text: "a".to_string(),
1191 cursor_id: CursorId(0),
1192 });
1193 log.append(Event::MoveCursor {
1194 cursor_id: CursorId(0),
1195 old_position: 0,
1196 new_position: 1,
1197 old_anchor: None,
1198 new_anchor: None,
1199 old_sticky_column: 0,
1200 new_sticky_column: 0,
1201 });
1202 assert_eq!(log.current_index(), 2);
1203
1204 let undo_events = log.undo();
1206 assert!(!undo_events.is_empty());
1207 assert_eq!(log.current_index(), 0);
1208 assert!(log.can_redo());
1209
1210 log.append(Event::MoveCursor {
1212 cursor_id: CursorId(0),
1213 old_position: 0,
1214 new_position: 0,
1215 old_anchor: None,
1216 new_anchor: None,
1217 old_sticky_column: 0,
1218 new_sticky_column: 0,
1219 });
1220 assert!(
1221 log.can_redo(),
1222 "Navigation after undo should preserve redo history"
1223 );
1224
1225 let redo_events = log.redo();
1227 assert!(
1228 !redo_events.is_empty(),
1229 "Redo should return events after navigation"
1230 );
1231 }
1232
1233 #[test]
1234 fn test_write_action_after_undo_clears_redo() {
1235 let mut log = EventLog::new();
1237
1238 log.append(Event::Insert {
1239 position: 0,
1240 text: "a".to_string(),
1241 cursor_id: CursorId(0),
1242 });
1243
1244 log.undo();
1245 assert!(log.can_redo());
1246
1247 log.append(Event::Insert {
1249 position: 0,
1250 text: "b".to_string(),
1251 cursor_id: CursorId(0),
1252 });
1253 assert!(
1254 !log.can_redo(),
1255 "Write action after undo should clear redo history"
1256 );
1257 }
1258
1259 #[test]
1268 fn test_is_at_saved_position_after_truncate() {
1269 let mut log = EventLog::new();
1270
1271 for i in 0..150 {
1273 log.append(Event::Insert {
1274 position: i,
1275 text: "x".to_string(),
1276 cursor_id: CursorId(0),
1277 });
1278 }
1279
1280 assert_eq!(log.entries().len(), 150);
1281 assert_eq!(log.current_index(), 150);
1282
1283 log.mark_saved();
1285
1286 for _ in 0..30 {
1288 log.undo();
1289 }
1290 assert_eq!(log.current_index(), 120);
1291 assert_eq!(log.entries().len(), 150);
1292
1293 log.append(Event::Insert {
1295 position: 0,
1296 text: "NEW".to_string(),
1297 cursor_id: CursorId(0),
1298 });
1299
1300 assert_eq!(log.entries().len(), 121);
1302 assert_eq!(log.current_index(), 121);
1303
1304 let result = log.is_at_saved_position();
1308
1309 assert!(
1311 !result,
1312 "Should not be at saved position after undo + new edit"
1313 );
1314 }
1315}