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 CancelAnchor {
72 cursor_id: CursorId,
73 },
74
75 ClearAnchor {
78 cursor_id: CursorId,
79 },
80
81 ChangeMode {
83 mode: String,
84 },
85
86 AddOverlay {
88 namespace: Option<OverlayNamespace>,
89 range: Range<usize>,
90 face: OverlayFace,
91 priority: i32,
92 message: Option<String>,
93 extend_to_line_end: bool,
95 url: Option<String>,
97 },
98
99 RemoveOverlay {
101 handle: OverlayHandle,
102 },
103
104 RemoveOverlaysInRange {
106 range: Range<usize>,
107 },
108
109 ClearNamespace {
111 namespace: OverlayNamespace,
112 },
113
114 ClearOverlays,
116
117 ShowPopup {
119 popup: PopupData,
120 },
121
122 HidePopup,
124
125 ClearPopups,
127
128 PopupSelectNext,
130 PopupSelectPrev,
131 PopupPageDown,
132 PopupPageUp,
133
134 AddMarginAnnotation {
137 line: usize,
138 position: MarginPositionData,
139 content: MarginContentData,
140 annotation_id: Option<String>,
141 },
142
143 RemoveMarginAnnotation {
145 annotation_id: String,
146 },
147
148 RemoveMarginAnnotationsAtLine {
150 line: usize,
151 position: MarginPositionData,
152 },
153
154 ClearMarginPosition {
156 position: MarginPositionData,
157 },
158
159 ClearMargins,
161
162 SetLineNumbers {
164 enabled: bool,
165 },
166
167 SplitPane {
170 direction: SplitDirection,
171 new_buffer_id: BufferId,
172 ratio: f32,
173 },
174
175 CloseSplit {
177 split_id: SplitId,
178 },
179
180 SetActiveSplit {
182 split_id: SplitId,
183 },
184
185 AdjustSplitRatio {
187 split_id: SplitId,
188 delta: f32,
189 },
190
191 NextSplit,
193
194 PrevSplit,
196
197 Batch {
200 events: Vec<Event>,
201 description: String,
202 },
203
204 BulkEdit {
211 #[serde(skip)]
213 old_snapshot: Option<Arc<BufferSnapshot>>,
214 #[serde(skip)]
216 new_snapshot: Option<Arc<BufferSnapshot>>,
217 old_cursors: Vec<(CursorId, usize, Option<usize>)>,
219 new_cursors: Vec<(CursorId, usize, Option<usize>)>,
221 description: String,
223 #[serde(default)]
228 edits: Vec<(usize, usize, usize)>,
229 #[serde(default)]
234 displaced_markers: Vec<(u64, usize)>,
235 },
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub enum OverlayFace {
241 Underline {
242 color: (u8, u8, u8), style: UnderlineStyle,
244 },
245 Background {
246 color: (u8, u8, u8),
247 },
248 Foreground {
249 color: (u8, u8, u8),
250 },
251 Style {
256 options: OverlayOptions,
257 },
258}
259
260impl OverlayFace {
261 pub fn from_options(options: OverlayOptions) -> Self {
263 OverlayFace::Style { options }
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269pub enum UnderlineStyle {
270 Straight,
271 Wavy,
272 Dotted,
273 Dashed,
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
281pub enum PopupKindHint {
282 Completion,
284 #[default]
286 List,
287 Text,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct PopupData {
294 #[serde(default)]
296 pub kind: PopupKindHint,
297 pub title: Option<String>,
298 #[serde(default)]
300 pub description: Option<String>,
301 #[serde(default)]
302 pub transient: bool,
303 pub content: PopupContentData,
304 pub position: PopupPositionData,
305 pub width: u16,
306 pub max_height: u16,
307 pub bordered: bool,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub enum PopupContentData {
313 Text(Vec<String>),
314 List {
315 items: Vec<PopupListItemData>,
316 selected: usize,
317 },
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322pub struct PopupListItemData {
323 pub text: String,
324 pub detail: Option<String>,
325 pub icon: Option<String>,
326 pub data: Option<String>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub enum PopupPositionData {
332 AtCursor,
333 BelowCursor,
334 AboveCursor,
335 Fixed {
336 x: u16,
337 y: u16,
338 },
339 Centered,
340 BottomRight,
341 AboveStatusBarAt {
345 x: u16,
346 status_row: u16,
351 },
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
356pub enum MarginPositionData {
357 Left,
358 Right,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize)]
363pub enum MarginContentData {
364 Text(String),
365 Symbol {
366 text: String,
367 color: Option<(u8, u8, u8)>, },
369 Empty,
370}
371
372impl Event {
373 pub fn inverse(&self) -> Option<Self> {
376 match self {
377 Self::Insert { position, text, .. } => {
378 let range = *position..(position + text.len());
379 Some(Self::Delete {
380 range,
381 deleted_text: text.clone(),
382 cursor_id: CursorId::UNDO_SENTINEL,
383 })
384 }
385 Self::Delete {
386 range,
387 deleted_text,
388 ..
389 } => Some(Self::Insert {
390 position: range.start,
391 text: deleted_text.clone(),
392 cursor_id: CursorId::UNDO_SENTINEL,
393 }),
394 Self::Batch {
395 events,
396 description,
397 } => {
398 let inverted: Option<Vec<Self>> =
400 events.iter().rev().map(|e| e.inverse()).collect();
401
402 inverted.map(|inverted_events| Self::Batch {
403 events: inverted_events,
404 description: format!("Undo: {}", description),
405 })
406 }
407 Self::AddCursor {
408 cursor_id,
409 position,
410 anchor,
411 } => {
412 Some(Self::RemoveCursor {
414 cursor_id: *cursor_id,
415 position: *position,
416 anchor: *anchor,
417 })
418 }
419 Self::RemoveCursor {
420 cursor_id,
421 position,
422 anchor,
423 } => {
424 Some(Self::AddCursor {
426 cursor_id: *cursor_id,
427 position: *position,
428 anchor: *anchor,
429 })
430 }
431 Self::MoveCursor {
432 cursor_id,
433 old_position,
434 new_position,
435 old_anchor,
436 new_anchor,
437 old_sticky_column,
438 new_sticky_column,
439 } => {
440 Some(Self::MoveCursor {
442 cursor_id: *cursor_id,
443 old_position: *new_position,
444 new_position: *old_position,
445 old_anchor: *new_anchor,
446 new_anchor: *old_anchor,
447 old_sticky_column: *new_sticky_column,
448 new_sticky_column: *old_sticky_column,
449 })
450 }
451 Self::AddOverlay { .. } => {
452 None
454 }
455 Self::RemoveOverlay { .. } => {
456 None
458 }
459 Self::ClearNamespace { .. } => {
460 None
462 }
463 Self::Scroll { line_offset } => Some(Self::Scroll {
464 line_offset: -line_offset,
465 }),
466 Self::SetViewport { top_line: _ } => {
467 None
469 }
470 Self::ChangeMode { mode: _ } => {
471 None
473 }
474 Self::BulkEdit {
475 old_snapshot,
476 new_snapshot,
477 old_cursors,
478 new_cursors,
479 description,
480 edits,
481 displaced_markers,
482 } => {
483 let inverted_edits: Vec<(usize, usize, usize)> = edits
486 .iter()
487 .map(|(pos, del_len, ins_len)| (*pos, *ins_len, *del_len))
488 .collect();
489
490 Some(Self::BulkEdit {
491 old_snapshot: new_snapshot.clone(),
492 new_snapshot: old_snapshot.clone(),
493 old_cursors: new_cursors.clone(),
494 new_cursors: old_cursors.clone(),
495 description: format!("Undo: {}", description),
496 edits: inverted_edits,
497 displaced_markers: displaced_markers.clone(),
501 })
502 }
503 _ => None,
505 }
506 }
507
508 pub fn modifies_buffer(&self) -> bool {
510 match self {
511 Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
512 Self::Batch { events, .. } => events.iter().any(|e| e.modifies_buffer()),
513 _ => false,
514 }
515 }
516
517 pub fn is_write_action(&self) -> bool {
530 match self {
531 Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
533
534 Self::AddCursor { .. } | Self::RemoveCursor { .. } => true,
536
537 Self::Batch { events, .. } => events.iter().any(|e| e.is_write_action()),
539
540 _ => false,
542 }
543 }
544
545 pub fn cursor_id(&self) -> Option<CursorId> {
547 match self {
548 Self::Insert { cursor_id, .. }
549 | Self::Delete { cursor_id, .. }
550 | Self::MoveCursor { cursor_id, .. }
551 | Self::AddCursor { cursor_id, .. }
552 | Self::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
553 _ => None,
554 }
555 }
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct LogEntry {
561 pub event: Event,
563
564 pub timestamp: u64,
566
567 pub description: Option<String>,
569
570 #[serde(default, skip_serializing_if = "Vec::is_empty")]
575 pub displaced_markers: Vec<(u64, usize)>,
576
577 #[serde(skip)]
582 pub group_id: Option<u64>,
583}
584
585impl LogEntry {
586 pub fn new(event: Event) -> Self {
587 Self {
588 event,
589 timestamp: std::time::SystemTime::now()
590 .duration_since(std::time::UNIX_EPOCH)
591 .unwrap()
592 .as_millis() as u64,
593 description: None,
594 displaced_markers: Vec::new(),
595 group_id: None,
596 }
597 }
598
599 pub fn with_description(mut self, description: String) -> Self {
600 self.description = Some(description);
601 self
602 }
603}
604
605#[derive(Debug, Clone)]
607pub struct Snapshot {
608 pub log_index: usize,
610
611 pub buffer_state: (),
614
615 pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
617}
618
619pub struct EventLog {
621 entries: Vec<LogEntry>,
623
624 current_index: usize,
626
627 snapshots: Vec<Snapshot>,
629
630 snapshot_interval: usize,
632
633 #[cfg(feature = "runtime")]
635 stream_file: Option<std::fs::File>,
636
637 saved_at_index: Option<usize>,
640
641 next_group_id: u64,
643
644 current_group: Option<u64>,
646
647 group_depth: u32,
650}
651
652impl EventLog {
653 pub fn new() -> Self {
655 Self {
656 entries: Vec::new(),
657 current_index: 0,
658 snapshots: Vec::new(),
659 snapshot_interval: 100,
660 #[cfg(feature = "runtime")]
661 stream_file: None,
662 saved_at_index: Some(0), next_group_id: 0,
664 current_group: None,
665 group_depth: 0,
666 }
667 }
668
669 pub fn begin_undo_group(&mut self) {
673 if self.group_depth == 0 {
674 self.current_group = Some(self.next_group_id);
675 self.next_group_id += 1;
676 }
677 self.group_depth += 1;
678 }
679
680 pub fn end_undo_group(&mut self) {
682 if self.group_depth > 0 {
683 self.group_depth -= 1;
684 if self.group_depth == 0 {
685 self.current_group = None;
686 }
687 }
688 }
689
690 pub fn mark_saved(&mut self) {
693 self.saved_at_index = Some(self.current_index);
694 }
695
696 pub fn clear_saved_position(&mut self) {
700 self.saved_at_index = None;
701 }
702
703 pub fn is_at_saved_position(&self) -> bool {
707 match self.saved_at_index {
708 None => false,
709 Some(saved_idx) if saved_idx == self.current_index => true,
710 Some(saved_idx) => {
711 let (start, end) = if saved_idx < self.current_index {
714 (saved_idx, self.current_index)
715 } else {
716 (self.current_index, saved_idx)
717 };
718
719 self.entries[start..end]
721 .iter()
722 .all(|entry| !entry.event.modifies_buffer())
723 }
724 }
725 }
726
727 #[cfg(feature = "runtime")]
729 pub fn enable_streaming<P: AsRef<std::path::Path>>(&mut self, path: P) -> std::io::Result<()> {
730 use std::io::Write;
731
732 let mut file = std::fs::OpenOptions::new()
733 .create(true)
734 .write(true)
735 .truncate(true)
736 .open(path)?;
737
738 writeln!(file, "# Event Log Stream")?;
740 writeln!(file, "# Started at: {}", chrono::Local::now())?;
741 writeln!(file, "# Format: JSON Lines (one event per line)")?;
742 writeln!(file, "#")?;
743
744 self.stream_file = Some(file);
745 Ok(())
746 }
747
748 #[cfg(feature = "runtime")]
750 pub fn disable_streaming(&mut self) {
751 self.stream_file = None;
752 }
753
754 #[cfg(feature = "runtime")]
756 pub fn log_render_state(
757 &mut self,
758 cursor_pos: usize,
759 screen_cursor_x: u16,
760 screen_cursor_y: u16,
761 buffer_len: usize,
762 ) {
763 if let Some(ref mut file) = self.stream_file {
764 use std::io::Write;
765
766 let render_info = serde_json::json!({
767 "type": "render",
768 "timestamp": chrono::Local::now().to_rfc3339(),
769 "cursor_position": cursor_pos,
770 "screen_cursor": {"x": screen_cursor_x, "y": screen_cursor_y},
771 "buffer_length": buffer_len,
772 });
773
774 if let Err(e) = writeln!(file, "{render_info}") {
775 tracing::trace!("Warning: Failed to write render info to stream: {e}");
776 }
777 if let Err(e) = file.flush() {
778 tracing::trace!("Warning: Failed to flush event stream: {e}");
779 }
780 }
781 }
782
783 #[cfg(feature = "runtime")]
785 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
786 if let Some(ref mut file) = self.stream_file {
787 use std::io::Write;
788
789 let keystroke_info = serde_json::json!({
790 "type": "keystroke",
791 "timestamp": chrono::Local::now().to_rfc3339(),
792 "key": key_code,
793 "modifiers": modifiers,
794 });
795
796 if let Err(e) = writeln!(file, "{keystroke_info}") {
797 tracing::trace!("Warning: Failed to write keystroke to stream: {e}");
798 }
799 if let Err(e) = file.flush() {
800 tracing::trace!("Warning: Failed to flush event stream: {e}");
801 }
802 }
803 }
804
805 pub fn append(&mut self, event: Event) -> usize {
807 if self.current_index < self.entries.len() {
813 if event.is_write_action() {
814 self.entries.truncate(self.current_index);
816
817 if let Some(saved_idx) = self.saved_at_index {
819 if saved_idx > self.current_index {
820 self.saved_at_index = None;
821 }
822 }
823 } else {
824 return self.current_index;
826 }
827 }
828
829 #[cfg(feature = "runtime")]
831 if let Some(ref mut file) = self.stream_file {
832 use std::io::Write;
833
834 let stream_entry = serde_json::json!({
835 "index": self.entries.len(),
836 "timestamp": chrono::Local::now().to_rfc3339(),
837 "event": event,
838 });
839
840 if let Err(e) = writeln!(file, "{stream_entry}") {
842 tracing::trace!("Warning: Failed to write to event stream: {e}");
843 }
844 if let Err(e) = file.flush() {
845 tracing::trace!("Warning: Failed to flush event stream: {e}");
846 }
847 }
848
849 let mut entry = LogEntry::new(event);
850 entry.group_id = self.current_group;
851 self.entries.push(entry);
852 self.current_index = self.entries.len();
853
854 if self.entries.len().is_multiple_of(self.snapshot_interval) {
856 }
859
860 self.current_index - 1
861 }
862
863 pub fn set_displaced_markers_on_last(&mut self, markers: Vec<(u64, usize)>) {
867 if let Some(entry) = self.entries.last_mut() {
868 entry.displaced_markers = markers;
869 }
870 }
871
872 pub fn current_index(&self) -> usize {
874 self.current_index
875 }
876
877 pub fn len(&self) -> usize {
879 self.entries.len()
880 }
881
882 pub fn is_empty(&self) -> bool {
884 self.entries.is_empty()
885 }
886
887 pub fn can_undo(&self) -> bool {
889 self.current_index > 0
890 }
891
892 pub fn can_redo(&self) -> bool {
894 self.current_index < self.entries.len()
895 }
896
897 pub fn undo(&mut self) -> Vec<(Event, Vec<(u64, usize)>)> {
903 let mut inverse_events = Vec::new();
904 let mut found_write_action = false;
905 let mut group: Option<u64> = None;
909
910 while self.can_undo() {
911 let idx = self.current_index - 1;
912 let is_write = self.entries[idx].event.is_write_action();
913 let entry_group = self.entries[idx].group_id;
914
915 if found_write_action {
916 match group {
917 None => break,
919 Some(g) if entry_group != Some(g) => break,
921 Some(_) => {}
922 }
923 }
924
925 self.current_index = idx;
926
927 if is_write && !found_write_action {
928 found_write_action = true;
929 group = entry_group;
930 }
931
932 if let Some(inverse) = self.entries[idx].event.inverse() {
934 inverse_events.push((inverse, self.entries[idx].displaced_markers.clone()));
935 }
936 }
938
939 inverse_events
940 }
941
942 pub fn redo(&mut self) -> Vec<Event> {
946 let mut events = Vec::new();
947 let mut found_write_action = false;
948 let mut group: Option<u64> = None;
951
952 while self.can_redo() {
954 let idx = self.current_index;
955 let is_write = self.entries[idx].event.is_write_action();
956 let entry_group = self.entries[idx].group_id;
957
958 if found_write_action && is_write {
961 match group {
962 None => break,
963 Some(g) if entry_group != Some(g) => break,
964 Some(_) => {}
965 }
966 }
967
968 if is_write && !found_write_action {
969 found_write_action = true;
970 group = entry_group;
971 }
972
973 events.push(self.entries[idx].event.clone());
974 self.current_index = idx + 1;
975 }
976
977 events
978 }
979
980 pub fn entries(&self) -> &[LogEntry] {
982 &self.entries
983 }
984
985 pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
987 &self.entries[range]
988 }
989
990 pub fn last_event(&self) -> Option<&Event> {
992 if self.current_index > 0 {
993 Some(&self.entries[self.current_index - 1].event)
994 } else {
995 None
996 }
997 }
998
999 pub fn clear(&mut self) {
1001 self.entries.clear();
1002 self.current_index = 0;
1003 self.snapshots.clear();
1004 }
1005
1006 pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
1008 use std::io::Write;
1009 let file = std::fs::File::create(path)?;
1010 let mut writer = std::io::BufWriter::new(file);
1011
1012 for entry in &self.entries {
1013 let json = serde_json::to_string(entry)?;
1014 writeln!(writer, "{json}")?;
1015 }
1016
1017 Ok(())
1018 }
1019
1020 pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
1022 use std::io::BufRead;
1023 let file = std::fs::File::open(path)?;
1024 let reader = std::io::BufReader::new(file);
1025
1026 let mut log = Self::new();
1027
1028 for line in reader.lines() {
1029 let line = line?;
1030 if line.trim().is_empty() {
1031 continue;
1032 }
1033 let entry: LogEntry = serde_json::from_str(&line)?;
1034 log.entries.push(entry);
1035 }
1036
1037 log.current_index = log.entries.len();
1038
1039 Ok(log)
1040 }
1041
1042 pub fn set_snapshot_interval(&mut self, interval: usize) {
1044 self.snapshot_interval = interval;
1045 }
1046}
1047
1048impl Default for EventLog {
1049 fn default() -> Self {
1050 Self::new()
1051 }
1052}
1053
1054#[cfg(test)]
1055mod tests {
1056 use super::*;
1057
1058 #[cfg(test)]
1060 mod property_tests {
1061 use super::*;
1062 use proptest::prelude::*;
1063
1064 fn arb_event() -> impl Strategy<Value = Event> {
1066 prop_oneof![
1067 (0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
1069 position: pos,
1070 text,
1071 cursor_id: CursorId(0),
1072 }),
1073 (0usize..1000, 1usize..50).prop_map(|(pos, len)| Event::Delete {
1075 range: pos..pos + len,
1076 deleted_text: "x".repeat(len),
1077 cursor_id: CursorId(0),
1078 }),
1079 ]
1080 }
1081
1082 proptest! {
1083 #[test]
1085 fn event_inverse_property(event in arb_event()) {
1086 if let Some(inverse) = event.inverse() {
1087 if let Some(double_inverse) = inverse.inverse() {
1090 match (&event, &double_inverse) {
1091 (Event::Insert { position: p1, text: t1, .. },
1092 Event::Insert { position: p2, text: t2, .. }) => {
1093 assert_eq!(p1, p2);
1094 assert_eq!(t1, t2);
1095 }
1096 (Event::Delete { range: r1, deleted_text: dt1, .. },
1097 Event::Delete { range: r2, deleted_text: dt2, .. }) => {
1098 assert_eq!(r1, r2);
1099 assert_eq!(dt1, dt2);
1100 }
1101 _ => {}
1102 }
1103 }
1104 }
1105 }
1106
1107 #[test]
1109 fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
1110 let mut log = EventLog::new();
1111
1112 for event in &events {
1114 log.append(event.clone());
1115 }
1116
1117 let after_append = log.current_index();
1118
1119 let mut undo_count = 0;
1121 while log.can_undo() {
1122 log.undo();
1123 undo_count += 1;
1124 }
1125
1126 assert_eq!(log.current_index(), 0);
1127 assert_eq!(undo_count, events.len());
1128
1129 let mut redo_count = 0;
1131 while log.can_redo() {
1132 log.redo();
1133 redo_count += 1;
1134 }
1135
1136 assert_eq!(log.current_index(), after_append);
1137 assert_eq!(redo_count, events.len());
1138 }
1139
1140 #[test]
1142 fn append_after_undo_truncates(
1143 initial_events in prop::collection::vec(arb_event(), 2..10),
1144 new_event in arb_event()
1145 ) {
1146 let mut log = EventLog::new();
1147
1148 for event in &initial_events {
1149 log.append(event.clone());
1150 }
1151
1152 log.undo();
1154 let index_after_undo = log.current_index();
1155
1156 log.append(new_event);
1158
1159 assert_eq!(log.current_index(), index_after_undo + 1);
1161 assert!(!log.can_redo());
1162 }
1163 }
1164 }
1165
1166 #[test]
1167 fn test_event_log_append() {
1168 let mut log = EventLog::new();
1169 let event = Event::Insert {
1170 position: 0,
1171 text: "hello".to_string(),
1172 cursor_id: CursorId(0),
1173 };
1174
1175 let index = log.append(event);
1176 assert_eq!(index, 0);
1177 assert_eq!(log.current_index(), 1);
1178 assert_eq!(log.entries().len(), 1);
1179 }
1180
1181 #[test]
1182 fn test_undo_redo() {
1183 let mut log = EventLog::new();
1184
1185 log.append(Event::Insert {
1186 position: 0,
1187 text: "a".to_string(),
1188 cursor_id: CursorId(0),
1189 });
1190
1191 log.append(Event::Insert {
1192 position: 1,
1193 text: "b".to_string(),
1194 cursor_id: CursorId(0),
1195 });
1196
1197 assert_eq!(log.current_index(), 2);
1198 assert!(log.can_undo());
1199 assert!(!log.can_redo());
1200
1201 log.undo();
1202 assert_eq!(log.current_index(), 1);
1203 assert!(log.can_undo());
1204 assert!(log.can_redo());
1205
1206 log.undo();
1207 assert_eq!(log.current_index(), 0);
1208 assert!(!log.can_undo());
1209 assert!(log.can_redo());
1210
1211 log.redo();
1212 assert_eq!(log.current_index(), 1);
1213 }
1214
1215 #[test]
1216 fn test_event_inverse() {
1217 let insert = Event::Insert {
1218 position: 5,
1219 text: "hello".to_string(),
1220 cursor_id: CursorId(0),
1221 };
1222
1223 let inverse = insert.inverse().unwrap();
1224 match inverse {
1225 Event::Delete {
1226 range,
1227 deleted_text,
1228 ..
1229 } => {
1230 assert_eq!(range, 5..10);
1231 assert_eq!(deleted_text, "hello");
1232 }
1233 _ => panic!("Expected Delete event"),
1234 }
1235 }
1236
1237 #[test]
1238 fn test_truncate_on_new_event_after_undo() {
1239 let mut log = EventLog::new();
1240
1241 log.append(Event::Insert {
1242 position: 0,
1243 text: "a".to_string(),
1244 cursor_id: CursorId(0),
1245 });
1246
1247 log.append(Event::Insert {
1248 position: 1,
1249 text: "b".to_string(),
1250 cursor_id: CursorId(0),
1251 });
1252
1253 log.undo();
1254 assert_eq!(log.entries().len(), 2);
1255
1256 log.append(Event::Insert {
1258 position: 1,
1259 text: "c".to_string(),
1260 cursor_id: CursorId(0),
1261 });
1262
1263 assert_eq!(log.entries().len(), 2);
1264 assert_eq!(log.current_index(), 2);
1265 }
1266
1267 #[test]
1268 fn test_navigation_after_undo_preserves_redo() {
1269 let mut log = EventLog::new();
1272
1273 log.append(Event::Insert {
1275 position: 0,
1276 text: "a".to_string(),
1277 cursor_id: CursorId(0),
1278 });
1279 log.append(Event::MoveCursor {
1280 cursor_id: CursorId(0),
1281 old_position: 0,
1282 new_position: 1,
1283 old_anchor: None,
1284 new_anchor: None,
1285 old_sticky_column: 0,
1286 new_sticky_column: 0,
1287 });
1288 assert_eq!(log.current_index(), 2);
1289
1290 let undo_events = log.undo();
1292 assert!(!undo_events.is_empty());
1293 assert_eq!(log.current_index(), 0);
1294 assert!(log.can_redo());
1295
1296 log.append(Event::MoveCursor {
1298 cursor_id: CursorId(0),
1299 old_position: 0,
1300 new_position: 0,
1301 old_anchor: None,
1302 new_anchor: None,
1303 old_sticky_column: 0,
1304 new_sticky_column: 0,
1305 });
1306 assert!(
1307 log.can_redo(),
1308 "Navigation after undo should preserve redo history"
1309 );
1310
1311 let redo_events = log.redo();
1313 assert!(
1314 !redo_events.is_empty(),
1315 "Redo should return events after navigation"
1316 );
1317 }
1318
1319 #[test]
1320 fn test_undo_group_reverts_and_reapplies_atomically() {
1321 let mut log = EventLog::new();
1324
1325 log.begin_undo_group();
1326 for (i, ch) in ['a', 'b', 'c'].into_iter().enumerate() {
1327 log.append(Event::Insert {
1328 position: i,
1329 text: ch.to_string(),
1330 cursor_id: CursorId(0),
1331 });
1332 }
1333 log.end_undo_group();
1334 assert_eq!(log.current_index(), 3);
1335
1336 let undo_events = log.undo();
1338 assert_eq!(undo_events.len(), 3, "all three inserts revert in one undo");
1339 assert_eq!(log.current_index(), 0);
1340 assert!(!log.can_undo());
1341
1342 let redo_events = log.redo();
1344 assert_eq!(
1345 redo_events.len(),
1346 3,
1347 "all three inserts reapply in one redo"
1348 );
1349 assert_eq!(log.current_index(), 3);
1350 assert!(!log.can_redo());
1351 }
1352
1353 #[test]
1354 fn test_ungrouped_edit_after_group_undoes_separately() {
1355 let mut log = EventLog::new();
1358
1359 log.begin_undo_group();
1360 log.append(Event::Insert {
1361 position: 0,
1362 text: "ab".to_string(),
1363 cursor_id: CursorId(0),
1364 });
1365 log.append(Event::Insert {
1366 position: 2,
1367 text: "cd".to_string(),
1368 cursor_id: CursorId(0),
1369 });
1370 log.end_undo_group();
1371
1372 log.append(Event::Insert {
1374 position: 4,
1375 text: "Z".to_string(),
1376 cursor_id: CursorId(0),
1377 });
1378 assert_eq!(log.current_index(), 3);
1379
1380 let first = log.undo();
1382 assert_eq!(first.len(), 1);
1383 assert_eq!(log.current_index(), 2);
1384
1385 let second = log.undo();
1387 assert_eq!(second.len(), 2);
1388 assert_eq!(log.current_index(), 0);
1389 }
1390
1391 #[test]
1392 fn test_consecutive_groups_undo_independently() {
1393 let mut log = EventLog::new();
1395
1396 for _ in 0..2 {
1397 log.begin_undo_group();
1398 log.append(Event::Insert {
1399 position: 0,
1400 text: "xy".to_string(),
1401 cursor_id: CursorId(0),
1402 });
1403 log.append(Event::Insert {
1404 position: 2,
1405 text: "zw".to_string(),
1406 cursor_id: CursorId(0),
1407 });
1408 log.end_undo_group();
1409 }
1410 assert_eq!(log.current_index(), 4);
1411
1412 let first = log.undo();
1413 assert_eq!(first.len(), 2, "second group reverts on its own");
1414 assert_eq!(log.current_index(), 2);
1415
1416 let second = log.undo();
1417 assert_eq!(second.len(), 2, "first group reverts on its own");
1418 assert_eq!(log.current_index(), 0);
1419 }
1420
1421 #[test]
1422 fn test_write_action_after_undo_clears_redo() {
1423 let mut log = EventLog::new();
1425
1426 log.append(Event::Insert {
1427 position: 0,
1428 text: "a".to_string(),
1429 cursor_id: CursorId(0),
1430 });
1431
1432 log.undo();
1433 assert!(log.can_redo());
1434
1435 log.append(Event::Insert {
1437 position: 0,
1438 text: "b".to_string(),
1439 cursor_id: CursorId(0),
1440 });
1441 assert!(
1442 !log.can_redo(),
1443 "Write action after undo should clear redo history"
1444 );
1445 }
1446
1447 #[test]
1456 fn test_is_at_saved_position_after_truncate() {
1457 let mut log = EventLog::new();
1458
1459 for i in 0..150 {
1461 log.append(Event::Insert {
1462 position: i,
1463 text: "x".to_string(),
1464 cursor_id: CursorId(0),
1465 });
1466 }
1467
1468 assert_eq!(log.entries().len(), 150);
1469 assert_eq!(log.current_index(), 150);
1470
1471 log.mark_saved();
1473
1474 for _ in 0..30 {
1476 log.undo();
1477 }
1478 assert_eq!(log.current_index(), 120);
1479 assert_eq!(log.entries().len(), 150);
1480
1481 log.append(Event::Insert {
1483 position: 0,
1484 text: "NEW".to_string(),
1485 cursor_id: CursorId(0),
1486 });
1487
1488 assert_eq!(log.entries().len(), 121);
1490 assert_eq!(log.current_index(), 121);
1491
1492 let result = log.is_at_saved_position();
1496
1497 assert!(
1499 !result,
1500 "Should not be at saved position after undo + new edit"
1501 );
1502 }
1503}