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 #[serde(skip)]
577 pub group_id: Option<u64>,
578}
579
580impl LogEntry {
581 pub fn new(event: Event) -> Self {
582 Self {
583 event,
584 timestamp: std::time::SystemTime::now()
585 .duration_since(std::time::UNIX_EPOCH)
586 .unwrap()
587 .as_millis() as u64,
588 description: None,
589 displaced_markers: Vec::new(),
590 group_id: None,
591 }
592 }
593
594 pub fn with_description(mut self, description: String) -> Self {
595 self.description = Some(description);
596 self
597 }
598}
599
600#[derive(Debug, Clone)]
602pub struct Snapshot {
603 pub log_index: usize,
605
606 pub buffer_state: (),
609
610 pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
612}
613
614pub struct EventLog {
616 entries: Vec<LogEntry>,
618
619 current_index: usize,
621
622 snapshots: Vec<Snapshot>,
624
625 snapshot_interval: usize,
627
628 #[cfg(feature = "runtime")]
630 stream_file: Option<std::fs::File>,
631
632 saved_at_index: Option<usize>,
635
636 next_group_id: u64,
638
639 current_group: Option<u64>,
641
642 group_depth: u32,
645}
646
647impl EventLog {
648 pub fn new() -> Self {
650 Self {
651 entries: Vec::new(),
652 current_index: 0,
653 snapshots: Vec::new(),
654 snapshot_interval: 100,
655 #[cfg(feature = "runtime")]
656 stream_file: None,
657 saved_at_index: Some(0), next_group_id: 0,
659 current_group: None,
660 group_depth: 0,
661 }
662 }
663
664 pub fn begin_undo_group(&mut self) {
668 if self.group_depth == 0 {
669 self.current_group = Some(self.next_group_id);
670 self.next_group_id += 1;
671 }
672 self.group_depth += 1;
673 }
674
675 pub fn end_undo_group(&mut self) {
677 if self.group_depth > 0 {
678 self.group_depth -= 1;
679 if self.group_depth == 0 {
680 self.current_group = None;
681 }
682 }
683 }
684
685 pub fn mark_saved(&mut self) {
688 self.saved_at_index = Some(self.current_index);
689 }
690
691 pub fn clear_saved_position(&mut self) {
695 self.saved_at_index = None;
696 }
697
698 pub fn is_at_saved_position(&self) -> bool {
702 match self.saved_at_index {
703 None => false,
704 Some(saved_idx) if saved_idx == self.current_index => true,
705 Some(saved_idx) => {
706 let (start, end) = if saved_idx < self.current_index {
709 (saved_idx, self.current_index)
710 } else {
711 (self.current_index, saved_idx)
712 };
713
714 self.entries[start..end]
716 .iter()
717 .all(|entry| !entry.event.modifies_buffer())
718 }
719 }
720 }
721
722 #[cfg(feature = "runtime")]
724 pub fn enable_streaming<P: AsRef<std::path::Path>>(&mut self, path: P) -> std::io::Result<()> {
725 use std::io::Write;
726
727 let mut file = std::fs::OpenOptions::new()
728 .create(true)
729 .write(true)
730 .truncate(true)
731 .open(path)?;
732
733 writeln!(file, "# Event Log Stream")?;
735 writeln!(file, "# Started at: {}", chrono::Local::now())?;
736 writeln!(file, "# Format: JSON Lines (one event per line)")?;
737 writeln!(file, "#")?;
738
739 self.stream_file = Some(file);
740 Ok(())
741 }
742
743 #[cfg(feature = "runtime")]
745 pub fn disable_streaming(&mut self) {
746 self.stream_file = None;
747 }
748
749 #[cfg(feature = "runtime")]
751 pub fn log_render_state(
752 &mut self,
753 cursor_pos: usize,
754 screen_cursor_x: u16,
755 screen_cursor_y: u16,
756 buffer_len: usize,
757 ) {
758 if let Some(ref mut file) = self.stream_file {
759 use std::io::Write;
760
761 let render_info = serde_json::json!({
762 "type": "render",
763 "timestamp": chrono::Local::now().to_rfc3339(),
764 "cursor_position": cursor_pos,
765 "screen_cursor": {"x": screen_cursor_x, "y": screen_cursor_y},
766 "buffer_length": buffer_len,
767 });
768
769 if let Err(e) = writeln!(file, "{render_info}") {
770 tracing::trace!("Warning: Failed to write render info to stream: {e}");
771 }
772 if let Err(e) = file.flush() {
773 tracing::trace!("Warning: Failed to flush event stream: {e}");
774 }
775 }
776 }
777
778 #[cfg(feature = "runtime")]
780 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
781 if let Some(ref mut file) = self.stream_file {
782 use std::io::Write;
783
784 let keystroke_info = serde_json::json!({
785 "type": "keystroke",
786 "timestamp": chrono::Local::now().to_rfc3339(),
787 "key": key_code,
788 "modifiers": modifiers,
789 });
790
791 if let Err(e) = writeln!(file, "{keystroke_info}") {
792 tracing::trace!("Warning: Failed to write keystroke to stream: {e}");
793 }
794 if let Err(e) = file.flush() {
795 tracing::trace!("Warning: Failed to flush event stream: {e}");
796 }
797 }
798 }
799
800 pub fn append(&mut self, event: Event) -> usize {
802 if self.current_index < self.entries.len() {
808 if event.is_write_action() {
809 self.entries.truncate(self.current_index);
811
812 if let Some(saved_idx) = self.saved_at_index {
814 if saved_idx > self.current_index {
815 self.saved_at_index = None;
816 }
817 }
818 } else {
819 return self.current_index;
821 }
822 }
823
824 #[cfg(feature = "runtime")]
826 if let Some(ref mut file) = self.stream_file {
827 use std::io::Write;
828
829 let stream_entry = serde_json::json!({
830 "index": self.entries.len(),
831 "timestamp": chrono::Local::now().to_rfc3339(),
832 "event": event,
833 });
834
835 if let Err(e) = writeln!(file, "{stream_entry}") {
837 tracing::trace!("Warning: Failed to write to event stream: {e}");
838 }
839 if let Err(e) = file.flush() {
840 tracing::trace!("Warning: Failed to flush event stream: {e}");
841 }
842 }
843
844 let mut entry = LogEntry::new(event);
845 entry.group_id = self.current_group;
846 self.entries.push(entry);
847 self.current_index = self.entries.len();
848
849 if self.entries.len().is_multiple_of(self.snapshot_interval) {
851 }
854
855 self.current_index - 1
856 }
857
858 pub fn set_displaced_markers_on_last(&mut self, markers: Vec<(u64, usize)>) {
862 if let Some(entry) = self.entries.last_mut() {
863 entry.displaced_markers = markers;
864 }
865 }
866
867 pub fn current_index(&self) -> usize {
869 self.current_index
870 }
871
872 pub fn len(&self) -> usize {
874 self.entries.len()
875 }
876
877 pub fn is_empty(&self) -> bool {
879 self.entries.is_empty()
880 }
881
882 pub fn can_undo(&self) -> bool {
884 self.current_index > 0
885 }
886
887 pub fn can_redo(&self) -> bool {
889 self.current_index < self.entries.len()
890 }
891
892 pub fn undo(&mut self) -> Vec<(Event, Vec<(u64, usize)>)> {
898 let mut inverse_events = Vec::new();
899 let mut found_write_action = false;
900 let mut group: Option<u64> = None;
904
905 while self.can_undo() {
906 let idx = self.current_index - 1;
907 let is_write = self.entries[idx].event.is_write_action();
908 let entry_group = self.entries[idx].group_id;
909
910 if found_write_action {
911 match group {
912 None => break,
914 Some(g) if entry_group != Some(g) => break,
916 Some(_) => {}
917 }
918 }
919
920 self.current_index = idx;
921
922 if is_write && !found_write_action {
923 found_write_action = true;
924 group = entry_group;
925 }
926
927 if let Some(inverse) = self.entries[idx].event.inverse() {
929 inverse_events.push((inverse, self.entries[idx].displaced_markers.clone()));
930 }
931 }
933
934 inverse_events
935 }
936
937 pub fn redo(&mut self) -> Vec<Event> {
941 let mut events = Vec::new();
942 let mut found_write_action = false;
943 let mut group: Option<u64> = None;
946
947 while self.can_redo() {
949 let idx = self.current_index;
950 let is_write = self.entries[idx].event.is_write_action();
951 let entry_group = self.entries[idx].group_id;
952
953 if found_write_action && is_write {
956 match group {
957 None => break,
958 Some(g) if entry_group != Some(g) => break,
959 Some(_) => {}
960 }
961 }
962
963 if is_write && !found_write_action {
964 found_write_action = true;
965 group = entry_group;
966 }
967
968 events.push(self.entries[idx].event.clone());
969 self.current_index = idx + 1;
970 }
971
972 events
973 }
974
975 pub fn entries(&self) -> &[LogEntry] {
977 &self.entries
978 }
979
980 pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
982 &self.entries[range]
983 }
984
985 pub fn last_event(&self) -> Option<&Event> {
987 if self.current_index > 0 {
988 Some(&self.entries[self.current_index - 1].event)
989 } else {
990 None
991 }
992 }
993
994 pub fn clear(&mut self) {
996 self.entries.clear();
997 self.current_index = 0;
998 self.snapshots.clear();
999 }
1000
1001 pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
1003 use std::io::Write;
1004 let file = std::fs::File::create(path)?;
1005 let mut writer = std::io::BufWriter::new(file);
1006
1007 for entry in &self.entries {
1008 let json = serde_json::to_string(entry)?;
1009 writeln!(writer, "{json}")?;
1010 }
1011
1012 Ok(())
1013 }
1014
1015 pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
1017 use std::io::BufRead;
1018 let file = std::fs::File::open(path)?;
1019 let reader = std::io::BufReader::new(file);
1020
1021 let mut log = Self::new();
1022
1023 for line in reader.lines() {
1024 let line = line?;
1025 if line.trim().is_empty() {
1026 continue;
1027 }
1028 let entry: LogEntry = serde_json::from_str(&line)?;
1029 log.entries.push(entry);
1030 }
1031
1032 log.current_index = log.entries.len();
1033
1034 Ok(log)
1035 }
1036
1037 pub fn set_snapshot_interval(&mut self, interval: usize) {
1039 self.snapshot_interval = interval;
1040 }
1041}
1042
1043impl Default for EventLog {
1044 fn default() -> Self {
1045 Self::new()
1046 }
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051 use super::*;
1052
1053 #[cfg(test)]
1055 mod property_tests {
1056 use super::*;
1057 use proptest::prelude::*;
1058
1059 fn arb_event() -> impl Strategy<Value = Event> {
1061 prop_oneof![
1062 (0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
1064 position: pos,
1065 text,
1066 cursor_id: CursorId(0),
1067 }),
1068 (0usize..1000, 1usize..50).prop_map(|(pos, len)| Event::Delete {
1070 range: pos..pos + len,
1071 deleted_text: "x".repeat(len),
1072 cursor_id: CursorId(0),
1073 }),
1074 ]
1075 }
1076
1077 proptest! {
1078 #[test]
1080 fn event_inverse_property(event in arb_event()) {
1081 if let Some(inverse) = event.inverse() {
1082 if let Some(double_inverse) = inverse.inverse() {
1085 match (&event, &double_inverse) {
1086 (Event::Insert { position: p1, text: t1, .. },
1087 Event::Insert { position: p2, text: t2, .. }) => {
1088 assert_eq!(p1, p2);
1089 assert_eq!(t1, t2);
1090 }
1091 (Event::Delete { range: r1, deleted_text: dt1, .. },
1092 Event::Delete { range: r2, deleted_text: dt2, .. }) => {
1093 assert_eq!(r1, r2);
1094 assert_eq!(dt1, dt2);
1095 }
1096 _ => {}
1097 }
1098 }
1099 }
1100 }
1101
1102 #[test]
1104 fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
1105 let mut log = EventLog::new();
1106
1107 for event in &events {
1109 log.append(event.clone());
1110 }
1111
1112 let after_append = log.current_index();
1113
1114 let mut undo_count = 0;
1116 while log.can_undo() {
1117 log.undo();
1118 undo_count += 1;
1119 }
1120
1121 assert_eq!(log.current_index(), 0);
1122 assert_eq!(undo_count, events.len());
1123
1124 let mut redo_count = 0;
1126 while log.can_redo() {
1127 log.redo();
1128 redo_count += 1;
1129 }
1130
1131 assert_eq!(log.current_index(), after_append);
1132 assert_eq!(redo_count, events.len());
1133 }
1134
1135 #[test]
1137 fn append_after_undo_truncates(
1138 initial_events in prop::collection::vec(arb_event(), 2..10),
1139 new_event in arb_event()
1140 ) {
1141 let mut log = EventLog::new();
1142
1143 for event in &initial_events {
1144 log.append(event.clone());
1145 }
1146
1147 log.undo();
1149 let index_after_undo = log.current_index();
1150
1151 log.append(new_event);
1153
1154 assert_eq!(log.current_index(), index_after_undo + 1);
1156 assert!(!log.can_redo());
1157 }
1158 }
1159 }
1160
1161 #[test]
1162 fn test_event_log_append() {
1163 let mut log = EventLog::new();
1164 let event = Event::Insert {
1165 position: 0,
1166 text: "hello".to_string(),
1167 cursor_id: CursorId(0),
1168 };
1169
1170 let index = log.append(event);
1171 assert_eq!(index, 0);
1172 assert_eq!(log.current_index(), 1);
1173 assert_eq!(log.entries().len(), 1);
1174 }
1175
1176 #[test]
1177 fn test_undo_redo() {
1178 let mut log = EventLog::new();
1179
1180 log.append(Event::Insert {
1181 position: 0,
1182 text: "a".to_string(),
1183 cursor_id: CursorId(0),
1184 });
1185
1186 log.append(Event::Insert {
1187 position: 1,
1188 text: "b".to_string(),
1189 cursor_id: CursorId(0),
1190 });
1191
1192 assert_eq!(log.current_index(), 2);
1193 assert!(log.can_undo());
1194 assert!(!log.can_redo());
1195
1196 log.undo();
1197 assert_eq!(log.current_index(), 1);
1198 assert!(log.can_undo());
1199 assert!(log.can_redo());
1200
1201 log.undo();
1202 assert_eq!(log.current_index(), 0);
1203 assert!(!log.can_undo());
1204 assert!(log.can_redo());
1205
1206 log.redo();
1207 assert_eq!(log.current_index(), 1);
1208 }
1209
1210 #[test]
1211 fn test_event_inverse() {
1212 let insert = Event::Insert {
1213 position: 5,
1214 text: "hello".to_string(),
1215 cursor_id: CursorId(0),
1216 };
1217
1218 let inverse = insert.inverse().unwrap();
1219 match inverse {
1220 Event::Delete {
1221 range,
1222 deleted_text,
1223 ..
1224 } => {
1225 assert_eq!(range, 5..10);
1226 assert_eq!(deleted_text, "hello");
1227 }
1228 _ => panic!("Expected Delete event"),
1229 }
1230 }
1231
1232 #[test]
1233 fn test_truncate_on_new_event_after_undo() {
1234 let mut log = EventLog::new();
1235
1236 log.append(Event::Insert {
1237 position: 0,
1238 text: "a".to_string(),
1239 cursor_id: CursorId(0),
1240 });
1241
1242 log.append(Event::Insert {
1243 position: 1,
1244 text: "b".to_string(),
1245 cursor_id: CursorId(0),
1246 });
1247
1248 log.undo();
1249 assert_eq!(log.entries().len(), 2);
1250
1251 log.append(Event::Insert {
1253 position: 1,
1254 text: "c".to_string(),
1255 cursor_id: CursorId(0),
1256 });
1257
1258 assert_eq!(log.entries().len(), 2);
1259 assert_eq!(log.current_index(), 2);
1260 }
1261
1262 #[test]
1263 fn test_navigation_after_undo_preserves_redo() {
1264 let mut log = EventLog::new();
1267
1268 log.append(Event::Insert {
1270 position: 0,
1271 text: "a".to_string(),
1272 cursor_id: CursorId(0),
1273 });
1274 log.append(Event::MoveCursor {
1275 cursor_id: CursorId(0),
1276 old_position: 0,
1277 new_position: 1,
1278 old_anchor: None,
1279 new_anchor: None,
1280 old_sticky_column: 0,
1281 new_sticky_column: 0,
1282 });
1283 assert_eq!(log.current_index(), 2);
1284
1285 let undo_events = log.undo();
1287 assert!(!undo_events.is_empty());
1288 assert_eq!(log.current_index(), 0);
1289 assert!(log.can_redo());
1290
1291 log.append(Event::MoveCursor {
1293 cursor_id: CursorId(0),
1294 old_position: 0,
1295 new_position: 0,
1296 old_anchor: None,
1297 new_anchor: None,
1298 old_sticky_column: 0,
1299 new_sticky_column: 0,
1300 });
1301 assert!(
1302 log.can_redo(),
1303 "Navigation after undo should preserve redo history"
1304 );
1305
1306 let redo_events = log.redo();
1308 assert!(
1309 !redo_events.is_empty(),
1310 "Redo should return events after navigation"
1311 );
1312 }
1313
1314 #[test]
1315 fn test_undo_group_reverts_and_reapplies_atomically() {
1316 let mut log = EventLog::new();
1319
1320 log.begin_undo_group();
1321 for (i, ch) in ['a', 'b', 'c'].into_iter().enumerate() {
1322 log.append(Event::Insert {
1323 position: i,
1324 text: ch.to_string(),
1325 cursor_id: CursorId(0),
1326 });
1327 }
1328 log.end_undo_group();
1329 assert_eq!(log.current_index(), 3);
1330
1331 let undo_events = log.undo();
1333 assert_eq!(undo_events.len(), 3, "all three inserts revert in one undo");
1334 assert_eq!(log.current_index(), 0);
1335 assert!(!log.can_undo());
1336
1337 let redo_events = log.redo();
1339 assert_eq!(
1340 redo_events.len(),
1341 3,
1342 "all three inserts reapply in one redo"
1343 );
1344 assert_eq!(log.current_index(), 3);
1345 assert!(!log.can_redo());
1346 }
1347
1348 #[test]
1349 fn test_ungrouped_edit_after_group_undoes_separately() {
1350 let mut log = EventLog::new();
1353
1354 log.begin_undo_group();
1355 log.append(Event::Insert {
1356 position: 0,
1357 text: "ab".to_string(),
1358 cursor_id: CursorId(0),
1359 });
1360 log.append(Event::Insert {
1361 position: 2,
1362 text: "cd".to_string(),
1363 cursor_id: CursorId(0),
1364 });
1365 log.end_undo_group();
1366
1367 log.append(Event::Insert {
1369 position: 4,
1370 text: "Z".to_string(),
1371 cursor_id: CursorId(0),
1372 });
1373 assert_eq!(log.current_index(), 3);
1374
1375 let first = log.undo();
1377 assert_eq!(first.len(), 1);
1378 assert_eq!(log.current_index(), 2);
1379
1380 let second = log.undo();
1382 assert_eq!(second.len(), 2);
1383 assert_eq!(log.current_index(), 0);
1384 }
1385
1386 #[test]
1387 fn test_consecutive_groups_undo_independently() {
1388 let mut log = EventLog::new();
1390
1391 for _ in 0..2 {
1392 log.begin_undo_group();
1393 log.append(Event::Insert {
1394 position: 0,
1395 text: "xy".to_string(),
1396 cursor_id: CursorId(0),
1397 });
1398 log.append(Event::Insert {
1399 position: 2,
1400 text: "zw".to_string(),
1401 cursor_id: CursorId(0),
1402 });
1403 log.end_undo_group();
1404 }
1405 assert_eq!(log.current_index(), 4);
1406
1407 let first = log.undo();
1408 assert_eq!(first.len(), 2, "second group reverts on its own");
1409 assert_eq!(log.current_index(), 2);
1410
1411 let second = log.undo();
1412 assert_eq!(second.len(), 2, "first group reverts on its own");
1413 assert_eq!(log.current_index(), 0);
1414 }
1415
1416 #[test]
1417 fn test_write_action_after_undo_clears_redo() {
1418 let mut log = EventLog::new();
1420
1421 log.append(Event::Insert {
1422 position: 0,
1423 text: "a".to_string(),
1424 cursor_id: CursorId(0),
1425 });
1426
1427 log.undo();
1428 assert!(log.can_redo());
1429
1430 log.append(Event::Insert {
1432 position: 0,
1433 text: "b".to_string(),
1434 cursor_id: CursorId(0),
1435 });
1436 assert!(
1437 !log.can_redo(),
1438 "Write action after undo should clear redo history"
1439 );
1440 }
1441
1442 #[test]
1451 fn test_is_at_saved_position_after_truncate() {
1452 let mut log = EventLog::new();
1453
1454 for i in 0..150 {
1456 log.append(Event::Insert {
1457 position: i,
1458 text: "x".to_string(),
1459 cursor_id: CursorId(0),
1460 });
1461 }
1462
1463 assert_eq!(log.entries().len(), 150);
1464 assert_eq!(log.current_index(), 150);
1465
1466 log.mark_saved();
1468
1469 for _ in 0..30 {
1471 log.undo();
1472 }
1473 assert_eq!(log.current_index(), 120);
1474 assert_eq!(log.entries().len(), 150);
1475
1476 log.append(Event::Insert {
1478 position: 0,
1479 text: "NEW".to_string(),
1480 cursor_id: CursorId(0),
1481 });
1482
1483 assert_eq!(log.entries().len(), 121);
1485 assert_eq!(log.current_index(), 121);
1486
1487 let result = log.is_at_saved_position();
1491
1492 assert!(
1494 !result,
1495 "Should not be at saved position after undo + new edit"
1496 );
1497 }
1498}