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 {
331 x: u16,
332 y: u16,
333 },
334 Centered,
335 BottomRight,
336 AboveStatusBarAt {
340 x: u16,
341 status_row: u16,
346 },
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
351pub enum MarginPositionData {
352 Left,
353 Right,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
358pub enum MarginContentData {
359 Text(String),
360 Symbol {
361 text: String,
362 color: Option<(u8, u8, u8)>, },
364 Empty,
365}
366
367impl Event {
368 pub fn inverse(&self) -> Option<Self> {
371 match self {
372 Self::Insert { position, text, .. } => {
373 let range = *position..(position + text.len());
374 Some(Self::Delete {
375 range,
376 deleted_text: text.clone(),
377 cursor_id: CursorId::UNDO_SENTINEL,
378 })
379 }
380 Self::Delete {
381 range,
382 deleted_text,
383 ..
384 } => Some(Self::Insert {
385 position: range.start,
386 text: deleted_text.clone(),
387 cursor_id: CursorId::UNDO_SENTINEL,
388 }),
389 Self::Batch {
390 events,
391 description,
392 } => {
393 let inverted: Option<Vec<Self>> =
395 events.iter().rev().map(|e| e.inverse()).collect();
396
397 inverted.map(|inverted_events| Self::Batch {
398 events: inverted_events,
399 description: format!("Undo: {}", description),
400 })
401 }
402 Self::AddCursor {
403 cursor_id,
404 position,
405 anchor,
406 } => {
407 Some(Self::RemoveCursor {
409 cursor_id: *cursor_id,
410 position: *position,
411 anchor: *anchor,
412 })
413 }
414 Self::RemoveCursor {
415 cursor_id,
416 position,
417 anchor,
418 } => {
419 Some(Self::AddCursor {
421 cursor_id: *cursor_id,
422 position: *position,
423 anchor: *anchor,
424 })
425 }
426 Self::MoveCursor {
427 cursor_id,
428 old_position,
429 new_position,
430 old_anchor,
431 new_anchor,
432 old_sticky_column,
433 new_sticky_column,
434 } => {
435 Some(Self::MoveCursor {
437 cursor_id: *cursor_id,
438 old_position: *new_position,
439 new_position: *old_position,
440 old_anchor: *new_anchor,
441 new_anchor: *old_anchor,
442 old_sticky_column: *new_sticky_column,
443 new_sticky_column: *old_sticky_column,
444 })
445 }
446 Self::AddOverlay { .. } => {
447 None
449 }
450 Self::RemoveOverlay { .. } => {
451 None
453 }
454 Self::ClearNamespace { .. } => {
455 None
457 }
458 Self::Scroll { line_offset } => Some(Self::Scroll {
459 line_offset: -line_offset,
460 }),
461 Self::SetViewport { top_line: _ } => {
462 None
464 }
465 Self::ChangeMode { mode: _ } => {
466 None
468 }
469 Self::BulkEdit {
470 old_snapshot,
471 new_snapshot,
472 old_cursors,
473 new_cursors,
474 description,
475 edits,
476 displaced_markers,
477 } => {
478 let inverted_edits: Vec<(usize, usize, usize)> = edits
481 .iter()
482 .map(|(pos, del_len, ins_len)| (*pos, *ins_len, *del_len))
483 .collect();
484
485 Some(Self::BulkEdit {
486 old_snapshot: new_snapshot.clone(),
487 new_snapshot: old_snapshot.clone(),
488 old_cursors: new_cursors.clone(),
489 new_cursors: old_cursors.clone(),
490 description: format!("Undo: {}", description),
491 edits: inverted_edits,
492 displaced_markers: displaced_markers.clone(),
496 })
497 }
498 _ => None,
500 }
501 }
502
503 pub fn modifies_buffer(&self) -> bool {
505 match self {
506 Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
507 Self::Batch { events, .. } => events.iter().any(|e| e.modifies_buffer()),
508 _ => false,
509 }
510 }
511
512 pub fn is_write_action(&self) -> bool {
525 match self {
526 Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
528
529 Self::AddCursor { .. } | Self::RemoveCursor { .. } => true,
531
532 Self::Batch { events, .. } => events.iter().any(|e| e.is_write_action()),
534
535 _ => false,
537 }
538 }
539
540 pub fn cursor_id(&self) -> Option<CursorId> {
542 match self {
543 Self::Insert { cursor_id, .. }
544 | Self::Delete { cursor_id, .. }
545 | Self::MoveCursor { cursor_id, .. }
546 | Self::AddCursor { cursor_id, .. }
547 | Self::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
548 _ => None,
549 }
550 }
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct LogEntry {
556 pub event: Event,
558
559 pub timestamp: u64,
561
562 pub description: Option<String>,
564
565 #[serde(default, skip_serializing_if = "Vec::is_empty")]
570 pub displaced_markers: Vec<(u64, usize)>,
571}
572
573impl LogEntry {
574 pub fn new(event: Event) -> Self {
575 Self {
576 event,
577 timestamp: std::time::SystemTime::now()
578 .duration_since(std::time::UNIX_EPOCH)
579 .unwrap()
580 .as_millis() as u64,
581 description: None,
582 displaced_markers: Vec::new(),
583 }
584 }
585
586 pub fn with_description(mut self, description: String) -> Self {
587 self.description = Some(description);
588 self
589 }
590}
591
592#[derive(Debug, Clone)]
594pub struct Snapshot {
595 pub log_index: usize,
597
598 pub buffer_state: (),
601
602 pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
604}
605
606pub struct EventLog {
608 entries: Vec<LogEntry>,
610
611 current_index: usize,
613
614 snapshots: Vec<Snapshot>,
616
617 snapshot_interval: usize,
619
620 #[cfg(feature = "runtime")]
622 stream_file: Option<std::fs::File>,
623
624 saved_at_index: Option<usize>,
627}
628
629impl EventLog {
630 pub fn new() -> Self {
632 Self {
633 entries: Vec::new(),
634 current_index: 0,
635 snapshots: Vec::new(),
636 snapshot_interval: 100,
637 #[cfg(feature = "runtime")]
638 stream_file: None,
639 saved_at_index: Some(0), }
641 }
642
643 pub fn mark_saved(&mut self) {
646 self.saved_at_index = Some(self.current_index);
647 }
648
649 pub fn clear_saved_position(&mut self) {
653 self.saved_at_index = None;
654 }
655
656 pub fn is_at_saved_position(&self) -> bool {
660 match self.saved_at_index {
661 None => false,
662 Some(saved_idx) if saved_idx == self.current_index => true,
663 Some(saved_idx) => {
664 let (start, end) = if saved_idx < self.current_index {
667 (saved_idx, self.current_index)
668 } else {
669 (self.current_index, saved_idx)
670 };
671
672 self.entries[start..end]
674 .iter()
675 .all(|entry| !entry.event.modifies_buffer())
676 }
677 }
678 }
679
680 #[cfg(feature = "runtime")]
682 pub fn enable_streaming<P: AsRef<std::path::Path>>(&mut self, path: P) -> std::io::Result<()> {
683 use std::io::Write;
684
685 let mut file = std::fs::OpenOptions::new()
686 .create(true)
687 .write(true)
688 .truncate(true)
689 .open(path)?;
690
691 writeln!(file, "# Event Log Stream")?;
693 writeln!(file, "# Started at: {}", chrono::Local::now())?;
694 writeln!(file, "# Format: JSON Lines (one event per line)")?;
695 writeln!(file, "#")?;
696
697 self.stream_file = Some(file);
698 Ok(())
699 }
700
701 #[cfg(feature = "runtime")]
703 pub fn disable_streaming(&mut self) {
704 self.stream_file = None;
705 }
706
707 #[cfg(feature = "runtime")]
709 pub fn log_render_state(
710 &mut self,
711 cursor_pos: usize,
712 screen_cursor_x: u16,
713 screen_cursor_y: u16,
714 buffer_len: usize,
715 ) {
716 if let Some(ref mut file) = self.stream_file {
717 use std::io::Write;
718
719 let render_info = serde_json::json!({
720 "type": "render",
721 "timestamp": chrono::Local::now().to_rfc3339(),
722 "cursor_position": cursor_pos,
723 "screen_cursor": {"x": screen_cursor_x, "y": screen_cursor_y},
724 "buffer_length": buffer_len,
725 });
726
727 if let Err(e) = writeln!(file, "{render_info}") {
728 tracing::trace!("Warning: Failed to write render info to stream: {e}");
729 }
730 if let Err(e) = file.flush() {
731 tracing::trace!("Warning: Failed to flush event stream: {e}");
732 }
733 }
734 }
735
736 #[cfg(feature = "runtime")]
738 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
739 if let Some(ref mut file) = self.stream_file {
740 use std::io::Write;
741
742 let keystroke_info = serde_json::json!({
743 "type": "keystroke",
744 "timestamp": chrono::Local::now().to_rfc3339(),
745 "key": key_code,
746 "modifiers": modifiers,
747 });
748
749 if let Err(e) = writeln!(file, "{keystroke_info}") {
750 tracing::trace!("Warning: Failed to write keystroke to stream: {e}");
751 }
752 if let Err(e) = file.flush() {
753 tracing::trace!("Warning: Failed to flush event stream: {e}");
754 }
755 }
756 }
757
758 pub fn append(&mut self, event: Event) -> usize {
760 if self.current_index < self.entries.len() {
766 if event.is_write_action() {
767 self.entries.truncate(self.current_index);
769
770 if let Some(saved_idx) = self.saved_at_index {
772 if saved_idx > self.current_index {
773 self.saved_at_index = None;
774 }
775 }
776 } else {
777 return self.current_index;
779 }
780 }
781
782 #[cfg(feature = "runtime")]
784 if let Some(ref mut file) = self.stream_file {
785 use std::io::Write;
786
787 let stream_entry = serde_json::json!({
788 "index": self.entries.len(),
789 "timestamp": chrono::Local::now().to_rfc3339(),
790 "event": event,
791 });
792
793 if let Err(e) = writeln!(file, "{stream_entry}") {
795 tracing::trace!("Warning: Failed to write to event stream: {e}");
796 }
797 if let Err(e) = file.flush() {
798 tracing::trace!("Warning: Failed to flush event stream: {e}");
799 }
800 }
801
802 let entry = LogEntry::new(event);
803 self.entries.push(entry);
804 self.current_index = self.entries.len();
805
806 if self.entries.len().is_multiple_of(self.snapshot_interval) {
808 }
811
812 self.current_index - 1
813 }
814
815 pub fn set_displaced_markers_on_last(&mut self, markers: Vec<(u64, usize)>) {
819 if let Some(entry) = self.entries.last_mut() {
820 entry.displaced_markers = markers;
821 }
822 }
823
824 pub fn current_index(&self) -> usize {
826 self.current_index
827 }
828
829 pub fn len(&self) -> usize {
831 self.entries.len()
832 }
833
834 pub fn is_empty(&self) -> bool {
836 self.entries.is_empty()
837 }
838
839 pub fn can_undo(&self) -> bool {
841 self.current_index > 0
842 }
843
844 pub fn can_redo(&self) -> bool {
846 self.current_index < self.entries.len()
847 }
848
849 pub fn undo(&mut self) -> Vec<(Event, Vec<(u64, usize)>)> {
855 let mut inverse_events = Vec::new();
856 let mut found_write_action = false;
857
858 while self.can_undo() && !found_write_action {
860 self.current_index -= 1;
861 let entry = &self.entries[self.current_index];
862
863 if entry.event.is_write_action() {
865 found_write_action = true;
866 }
867
868 if let Some(inverse) = entry.event.inverse() {
870 inverse_events.push((inverse, entry.displaced_markers.clone()));
871 }
872 }
874
875 inverse_events
876 }
877
878 pub fn redo(&mut self) -> Vec<Event> {
882 let mut events = Vec::new();
883 let mut found_write_action = false;
884
885 while self.can_redo() {
887 let event = self.entries[self.current_index].event.clone();
888
889 if found_write_action && event.is_write_action() {
891 break;
893 }
894
895 self.current_index += 1;
896
897 if event.is_write_action() {
899 found_write_action = true;
900 }
901
902 events.push(event);
903 }
904
905 events
906 }
907
908 pub fn entries(&self) -> &[LogEntry] {
910 &self.entries
911 }
912
913 pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
915 &self.entries[range]
916 }
917
918 pub fn last_event(&self) -> Option<&Event> {
920 if self.current_index > 0 {
921 Some(&self.entries[self.current_index - 1].event)
922 } else {
923 None
924 }
925 }
926
927 pub fn clear(&mut self) {
929 self.entries.clear();
930 self.current_index = 0;
931 self.snapshots.clear();
932 }
933
934 pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
936 use std::io::Write;
937 let file = std::fs::File::create(path)?;
938 let mut writer = std::io::BufWriter::new(file);
939
940 for entry in &self.entries {
941 let json = serde_json::to_string(entry)?;
942 writeln!(writer, "{json}")?;
943 }
944
945 Ok(())
946 }
947
948 pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
950 use std::io::BufRead;
951 let file = std::fs::File::open(path)?;
952 let reader = std::io::BufReader::new(file);
953
954 let mut log = Self::new();
955
956 for line in reader.lines() {
957 let line = line?;
958 if line.trim().is_empty() {
959 continue;
960 }
961 let entry: LogEntry = serde_json::from_str(&line)?;
962 log.entries.push(entry);
963 }
964
965 log.current_index = log.entries.len();
966
967 Ok(log)
968 }
969
970 pub fn set_snapshot_interval(&mut self, interval: usize) {
972 self.snapshot_interval = interval;
973 }
974}
975
976impl Default for EventLog {
977 fn default() -> Self {
978 Self::new()
979 }
980}
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985
986 #[cfg(test)]
988 mod property_tests {
989 use super::*;
990 use proptest::prelude::*;
991
992 fn arb_event() -> impl Strategy<Value = Event> {
994 prop_oneof![
995 (0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
997 position: pos,
998 text,
999 cursor_id: CursorId(0),
1000 }),
1001 (0usize..1000, 1usize..50).prop_map(|(pos, len)| Event::Delete {
1003 range: pos..pos + len,
1004 deleted_text: "x".repeat(len),
1005 cursor_id: CursorId(0),
1006 }),
1007 ]
1008 }
1009
1010 proptest! {
1011 #[test]
1013 fn event_inverse_property(event in arb_event()) {
1014 if let Some(inverse) = event.inverse() {
1015 if let Some(double_inverse) = inverse.inverse() {
1018 match (&event, &double_inverse) {
1019 (Event::Insert { position: p1, text: t1, .. },
1020 Event::Insert { position: p2, text: t2, .. }) => {
1021 assert_eq!(p1, p2);
1022 assert_eq!(t1, t2);
1023 }
1024 (Event::Delete { range: r1, deleted_text: dt1, .. },
1025 Event::Delete { range: r2, deleted_text: dt2, .. }) => {
1026 assert_eq!(r1, r2);
1027 assert_eq!(dt1, dt2);
1028 }
1029 _ => {}
1030 }
1031 }
1032 }
1033 }
1034
1035 #[test]
1037 fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
1038 let mut log = EventLog::new();
1039
1040 for event in &events {
1042 log.append(event.clone());
1043 }
1044
1045 let after_append = log.current_index();
1046
1047 let mut undo_count = 0;
1049 while log.can_undo() {
1050 log.undo();
1051 undo_count += 1;
1052 }
1053
1054 assert_eq!(log.current_index(), 0);
1055 assert_eq!(undo_count, events.len());
1056
1057 let mut redo_count = 0;
1059 while log.can_redo() {
1060 log.redo();
1061 redo_count += 1;
1062 }
1063
1064 assert_eq!(log.current_index(), after_append);
1065 assert_eq!(redo_count, events.len());
1066 }
1067
1068 #[test]
1070 fn append_after_undo_truncates(
1071 initial_events in prop::collection::vec(arb_event(), 2..10),
1072 new_event in arb_event()
1073 ) {
1074 let mut log = EventLog::new();
1075
1076 for event in &initial_events {
1077 log.append(event.clone());
1078 }
1079
1080 log.undo();
1082 let index_after_undo = log.current_index();
1083
1084 log.append(new_event);
1086
1087 assert_eq!(log.current_index(), index_after_undo + 1);
1089 assert!(!log.can_redo());
1090 }
1091 }
1092 }
1093
1094 #[test]
1095 fn test_event_log_append() {
1096 let mut log = EventLog::new();
1097 let event = Event::Insert {
1098 position: 0,
1099 text: "hello".to_string(),
1100 cursor_id: CursorId(0),
1101 };
1102
1103 let index = log.append(event);
1104 assert_eq!(index, 0);
1105 assert_eq!(log.current_index(), 1);
1106 assert_eq!(log.entries().len(), 1);
1107 }
1108
1109 #[test]
1110 fn test_undo_redo() {
1111 let mut log = EventLog::new();
1112
1113 log.append(Event::Insert {
1114 position: 0,
1115 text: "a".to_string(),
1116 cursor_id: CursorId(0),
1117 });
1118
1119 log.append(Event::Insert {
1120 position: 1,
1121 text: "b".to_string(),
1122 cursor_id: CursorId(0),
1123 });
1124
1125 assert_eq!(log.current_index(), 2);
1126 assert!(log.can_undo());
1127 assert!(!log.can_redo());
1128
1129 log.undo();
1130 assert_eq!(log.current_index(), 1);
1131 assert!(log.can_undo());
1132 assert!(log.can_redo());
1133
1134 log.undo();
1135 assert_eq!(log.current_index(), 0);
1136 assert!(!log.can_undo());
1137 assert!(log.can_redo());
1138
1139 log.redo();
1140 assert_eq!(log.current_index(), 1);
1141 }
1142
1143 #[test]
1144 fn test_event_inverse() {
1145 let insert = Event::Insert {
1146 position: 5,
1147 text: "hello".to_string(),
1148 cursor_id: CursorId(0),
1149 };
1150
1151 let inverse = insert.inverse().unwrap();
1152 match inverse {
1153 Event::Delete {
1154 range,
1155 deleted_text,
1156 ..
1157 } => {
1158 assert_eq!(range, 5..10);
1159 assert_eq!(deleted_text, "hello");
1160 }
1161 _ => panic!("Expected Delete event"),
1162 }
1163 }
1164
1165 #[test]
1166 fn test_truncate_on_new_event_after_undo() {
1167 let mut log = EventLog::new();
1168
1169 log.append(Event::Insert {
1170 position: 0,
1171 text: "a".to_string(),
1172 cursor_id: CursorId(0),
1173 });
1174
1175 log.append(Event::Insert {
1176 position: 1,
1177 text: "b".to_string(),
1178 cursor_id: CursorId(0),
1179 });
1180
1181 log.undo();
1182 assert_eq!(log.entries().len(), 2);
1183
1184 log.append(Event::Insert {
1186 position: 1,
1187 text: "c".to_string(),
1188 cursor_id: CursorId(0),
1189 });
1190
1191 assert_eq!(log.entries().len(), 2);
1192 assert_eq!(log.current_index(), 2);
1193 }
1194
1195 #[test]
1196 fn test_navigation_after_undo_preserves_redo() {
1197 let mut log = EventLog::new();
1200
1201 log.append(Event::Insert {
1203 position: 0,
1204 text: "a".to_string(),
1205 cursor_id: CursorId(0),
1206 });
1207 log.append(Event::MoveCursor {
1208 cursor_id: CursorId(0),
1209 old_position: 0,
1210 new_position: 1,
1211 old_anchor: None,
1212 new_anchor: None,
1213 old_sticky_column: 0,
1214 new_sticky_column: 0,
1215 });
1216 assert_eq!(log.current_index(), 2);
1217
1218 let undo_events = log.undo();
1220 assert!(!undo_events.is_empty());
1221 assert_eq!(log.current_index(), 0);
1222 assert!(log.can_redo());
1223
1224 log.append(Event::MoveCursor {
1226 cursor_id: CursorId(0),
1227 old_position: 0,
1228 new_position: 0,
1229 old_anchor: None,
1230 new_anchor: None,
1231 old_sticky_column: 0,
1232 new_sticky_column: 0,
1233 });
1234 assert!(
1235 log.can_redo(),
1236 "Navigation after undo should preserve redo history"
1237 );
1238
1239 let redo_events = log.redo();
1241 assert!(
1242 !redo_events.is_empty(),
1243 "Redo should return events after navigation"
1244 );
1245 }
1246
1247 #[test]
1248 fn test_write_action_after_undo_clears_redo() {
1249 let mut log = EventLog::new();
1251
1252 log.append(Event::Insert {
1253 position: 0,
1254 text: "a".to_string(),
1255 cursor_id: CursorId(0),
1256 });
1257
1258 log.undo();
1259 assert!(log.can_redo());
1260
1261 log.append(Event::Insert {
1263 position: 0,
1264 text: "b".to_string(),
1265 cursor_id: CursorId(0),
1266 });
1267 assert!(
1268 !log.can_redo(),
1269 "Write action after undo should clear redo history"
1270 );
1271 }
1272
1273 #[test]
1282 fn test_is_at_saved_position_after_truncate() {
1283 let mut log = EventLog::new();
1284
1285 for i in 0..150 {
1287 log.append(Event::Insert {
1288 position: i,
1289 text: "x".to_string(),
1290 cursor_id: CursorId(0),
1291 });
1292 }
1293
1294 assert_eq!(log.entries().len(), 150);
1295 assert_eq!(log.current_index(), 150);
1296
1297 log.mark_saved();
1299
1300 for _ in 0..30 {
1302 log.undo();
1303 }
1304 assert_eq!(log.current_index(), 120);
1305 assert_eq!(log.entries().len(), 150);
1306
1307 log.append(Event::Insert {
1309 position: 0,
1310 text: "NEW".to_string(),
1311 cursor_id: CursorId(0),
1312 });
1313
1314 assert_eq!(log.entries().len(), 121);
1316 assert_eq!(log.current_index(), 121);
1317
1318 let result = log.is_at_saved_position();
1322
1323 assert!(
1325 !result,
1326 "Should not be at saved position after undo + new edit"
1327 );
1328 }
1329}