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 },
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub enum OverlayFace {
224 Underline {
225 color: (u8, u8, u8), style: UnderlineStyle,
227 },
228 Background {
229 color: (u8, u8, u8),
230 },
231 Foreground {
232 color: (u8, u8, u8),
233 },
234 Style {
239 options: OverlayOptions,
240 },
241}
242
243impl OverlayFace {
244 pub fn from_options(options: OverlayOptions) -> Self {
246 OverlayFace::Style { options }
247 }
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub enum UnderlineStyle {
253 Straight,
254 Wavy,
255 Dotted,
256 Dashed,
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
264pub enum PopupKindHint {
265 Completion,
267 #[default]
269 List,
270 Text,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct PopupData {
277 #[serde(default)]
279 pub kind: PopupKindHint,
280 pub title: Option<String>,
281 #[serde(default)]
283 pub description: Option<String>,
284 #[serde(default)]
285 pub transient: bool,
286 pub content: PopupContentData,
287 pub position: PopupPositionData,
288 pub width: u16,
289 pub max_height: u16,
290 pub bordered: bool,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub enum PopupContentData {
296 Text(Vec<String>),
297 List {
298 items: Vec<PopupListItemData>,
299 selected: usize,
300 },
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct PopupListItemData {
306 pub text: String,
307 pub detail: Option<String>,
308 pub icon: Option<String>,
309 pub data: Option<String>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub enum PopupPositionData {
315 AtCursor,
316 BelowCursor,
317 AboveCursor,
318 Fixed { x: u16, y: u16 },
319 Centered,
320 BottomRight,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
325pub enum MarginPositionData {
326 Left,
327 Right,
328}
329
330#[derive(Debug, Clone, Serialize, Deserialize)]
332pub enum MarginContentData {
333 Text(String),
334 Symbol {
335 text: String,
336 color: Option<(u8, u8, u8)>, },
338 Empty,
339}
340
341impl Event {
342 pub fn inverse(&self) -> Option<Self> {
345 match self {
346 Self::Insert { position, text, .. } => {
347 let range = *position..(position + text.len());
348 Some(Self::Delete {
349 range,
350 deleted_text: text.clone(),
351 cursor_id: CursorId::UNDO_SENTINEL,
352 })
353 }
354 Self::Delete {
355 range,
356 deleted_text,
357 ..
358 } => Some(Self::Insert {
359 position: range.start,
360 text: deleted_text.clone(),
361 cursor_id: CursorId::UNDO_SENTINEL,
362 }),
363 Self::Batch {
364 events,
365 description,
366 } => {
367 let inverted: Option<Vec<Self>> =
369 events.iter().rev().map(|e| e.inverse()).collect();
370
371 inverted.map(|inverted_events| Self::Batch {
372 events: inverted_events,
373 description: format!("Undo: {}", description),
374 })
375 }
376 Self::AddCursor {
377 cursor_id,
378 position,
379 anchor,
380 } => {
381 Some(Self::RemoveCursor {
383 cursor_id: *cursor_id,
384 position: *position,
385 anchor: *anchor,
386 })
387 }
388 Self::RemoveCursor {
389 cursor_id,
390 position,
391 anchor,
392 } => {
393 Some(Self::AddCursor {
395 cursor_id: *cursor_id,
396 position: *position,
397 anchor: *anchor,
398 })
399 }
400 Self::MoveCursor {
401 cursor_id,
402 old_position,
403 new_position,
404 old_anchor,
405 new_anchor,
406 old_sticky_column,
407 new_sticky_column,
408 } => {
409 Some(Self::MoveCursor {
411 cursor_id: *cursor_id,
412 old_position: *new_position,
413 new_position: *old_position,
414 old_anchor: *new_anchor,
415 new_anchor: *old_anchor,
416 old_sticky_column: *new_sticky_column,
417 new_sticky_column: *old_sticky_column,
418 })
419 }
420 Self::AddOverlay { .. } => {
421 None
423 }
424 Self::RemoveOverlay { .. } => {
425 None
427 }
428 Self::ClearNamespace { .. } => {
429 None
431 }
432 Self::Scroll { line_offset } => Some(Self::Scroll {
433 line_offset: -line_offset,
434 }),
435 Self::SetViewport { top_line: _ } => {
436 None
438 }
439 Self::ChangeMode { mode: _ } => {
440 None
442 }
443 Self::BulkEdit {
444 old_snapshot,
445 new_snapshot,
446 old_cursors,
447 new_cursors,
448 description,
449 } => {
450 Some(Self::BulkEdit {
453 old_snapshot: new_snapshot.clone(),
454 new_snapshot: old_snapshot.clone(),
455 old_cursors: new_cursors.clone(),
456 new_cursors: old_cursors.clone(),
457 description: format!("Undo: {}", description),
458 })
459 }
460 _ => None,
462 }
463 }
464
465 pub fn modifies_buffer(&self) -> bool {
467 match self {
468 Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
469 Self::Batch { events, .. } => events.iter().any(|e| e.modifies_buffer()),
470 _ => false,
471 }
472 }
473
474 pub fn is_write_action(&self) -> bool {
487 match self {
488 Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
490
491 Self::AddCursor { .. } | Self::RemoveCursor { .. } => true,
493
494 Self::Batch { events, .. } => events.iter().any(|e| e.is_write_action()),
496
497 _ => false,
499 }
500 }
501
502 pub fn cursor_id(&self) -> Option<CursorId> {
504 match self {
505 Self::Insert { cursor_id, .. }
506 | Self::Delete { cursor_id, .. }
507 | Self::MoveCursor { cursor_id, .. }
508 | Self::AddCursor { cursor_id, .. }
509 | Self::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
510 _ => None,
511 }
512 }
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct LogEntry {
518 pub event: Event,
520
521 pub timestamp: u64,
523
524 pub description: Option<String>,
526}
527
528impl LogEntry {
529 pub fn new(event: Event) -> Self {
530 Self {
531 event,
532 timestamp: std::time::SystemTime::now()
533 .duration_since(std::time::UNIX_EPOCH)
534 .unwrap()
535 .as_millis() as u64,
536 description: None,
537 }
538 }
539
540 pub fn with_description(mut self, description: String) -> Self {
541 self.description = Some(description);
542 self
543 }
544}
545
546#[derive(Debug, Clone)]
548pub struct Snapshot {
549 pub log_index: usize,
551
552 pub buffer_state: (),
555
556 pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
558}
559
560pub struct EventLog {
562 entries: Vec<LogEntry>,
564
565 current_index: usize,
567
568 snapshots: Vec<Snapshot>,
570
571 snapshot_interval: usize,
573
574 #[cfg(feature = "runtime")]
576 stream_file: Option<std::fs::File>,
577
578 saved_at_index: Option<usize>,
581}
582
583impl EventLog {
584 pub fn new() -> Self {
586 Self {
587 entries: Vec::new(),
588 current_index: 0,
589 snapshots: Vec::new(),
590 snapshot_interval: 100,
591 #[cfg(feature = "runtime")]
592 stream_file: None,
593 saved_at_index: Some(0), }
595 }
596
597 pub fn mark_saved(&mut self) {
600 self.saved_at_index = Some(self.current_index);
601 }
602
603 pub fn is_at_saved_position(&self) -> bool {
607 match self.saved_at_index {
608 None => false,
609 Some(saved_idx) if saved_idx == self.current_index => true,
610 Some(saved_idx) => {
611 let (start, end) = if saved_idx < self.current_index {
614 (saved_idx, self.current_index)
615 } else {
616 (self.current_index, saved_idx)
617 };
618
619 self.entries[start..end]
621 .iter()
622 .all(|entry| !entry.event.modifies_buffer())
623 }
624 }
625 }
626
627 #[cfg(feature = "runtime")]
629 pub fn enable_streaming<P: AsRef<std::path::Path>>(&mut self, path: P) -> std::io::Result<()> {
630 use std::io::Write;
631
632 let mut file = std::fs::OpenOptions::new()
633 .create(true)
634 .write(true)
635 .truncate(true)
636 .open(path)?;
637
638 writeln!(file, "# Event Log Stream")?;
640 writeln!(file, "# Started at: {}", chrono::Local::now())?;
641 writeln!(file, "# Format: JSON Lines (one event per line)")?;
642 writeln!(file, "#")?;
643
644 self.stream_file = Some(file);
645 Ok(())
646 }
647
648 #[cfg(feature = "runtime")]
650 pub fn disable_streaming(&mut self) {
651 self.stream_file = None;
652 }
653
654 #[cfg(feature = "runtime")]
656 pub fn log_render_state(
657 &mut self,
658 cursor_pos: usize,
659 screen_cursor_x: u16,
660 screen_cursor_y: u16,
661 buffer_len: usize,
662 ) {
663 if let Some(ref mut file) = self.stream_file {
664 use std::io::Write;
665
666 let render_info = serde_json::json!({
667 "type": "render",
668 "timestamp": chrono::Local::now().to_rfc3339(),
669 "cursor_position": cursor_pos,
670 "screen_cursor": {"x": screen_cursor_x, "y": screen_cursor_y},
671 "buffer_length": buffer_len,
672 });
673
674 if let Err(e) = writeln!(file, "{render_info}") {
675 tracing::trace!("Warning: Failed to write render info to stream: {e}");
676 }
677 if let Err(e) = file.flush() {
678 tracing::trace!("Warning: Failed to flush event stream: {e}");
679 }
680 }
681 }
682
683 #[cfg(feature = "runtime")]
685 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
686 if let Some(ref mut file) = self.stream_file {
687 use std::io::Write;
688
689 let keystroke_info = serde_json::json!({
690 "type": "keystroke",
691 "timestamp": chrono::Local::now().to_rfc3339(),
692 "key": key_code,
693 "modifiers": modifiers,
694 });
695
696 if let Err(e) = writeln!(file, "{keystroke_info}") {
697 tracing::trace!("Warning: Failed to write keystroke to stream: {e}");
698 }
699 if let Err(e) = file.flush() {
700 tracing::trace!("Warning: Failed to flush event stream: {e}");
701 }
702 }
703 }
704
705 pub fn append(&mut self, event: Event) -> usize {
707 if self.current_index < self.entries.len() {
713 if event.is_write_action() {
714 self.entries.truncate(self.current_index);
716
717 if let Some(saved_idx) = self.saved_at_index {
719 if saved_idx > self.current_index {
720 self.saved_at_index = None;
721 }
722 }
723 } else {
724 return self.current_index;
726 }
727 }
728
729 #[cfg(feature = "runtime")]
731 if let Some(ref mut file) = self.stream_file {
732 use std::io::Write;
733
734 let stream_entry = serde_json::json!({
735 "index": self.entries.len(),
736 "timestamp": chrono::Local::now().to_rfc3339(),
737 "event": event,
738 });
739
740 if let Err(e) = writeln!(file, "{stream_entry}") {
742 tracing::trace!("Warning: Failed to write to event stream: {e}");
743 }
744 if let Err(e) = file.flush() {
745 tracing::trace!("Warning: Failed to flush event stream: {e}");
746 }
747 }
748
749 let entry = LogEntry::new(event);
750 self.entries.push(entry);
751 self.current_index = self.entries.len();
752
753 if self.entries.len().is_multiple_of(self.snapshot_interval) {
755 }
758
759 self.current_index - 1
760 }
761
762 pub fn current_index(&self) -> usize {
764 self.current_index
765 }
766
767 pub fn len(&self) -> usize {
769 self.entries.len()
770 }
771
772 pub fn is_empty(&self) -> bool {
774 self.entries.is_empty()
775 }
776
777 pub fn can_undo(&self) -> bool {
779 self.current_index > 0
780 }
781
782 pub fn can_redo(&self) -> bool {
784 self.current_index < self.entries.len()
785 }
786
787 pub fn undo(&mut self) -> Vec<Event> {
791 let mut inverse_events = Vec::new();
792 let mut found_write_action = false;
793
794 while self.can_undo() && !found_write_action {
796 self.current_index -= 1;
797 let event = &self.entries[self.current_index].event;
798
799 if event.is_write_action() {
801 found_write_action = true;
802 }
803
804 if let Some(inverse) = event.inverse() {
806 inverse_events.push(inverse);
807 }
808 }
810
811 inverse_events
812 }
813
814 pub fn redo(&mut self) -> Vec<Event> {
818 let mut events = Vec::new();
819 let mut found_write_action = false;
820
821 while self.can_redo() {
823 let event = self.entries[self.current_index].event.clone();
824
825 if found_write_action && event.is_write_action() {
827 break;
829 }
830
831 self.current_index += 1;
832
833 if event.is_write_action() {
835 found_write_action = true;
836 }
837
838 events.push(event);
839 }
840
841 events
842 }
843
844 pub fn entries(&self) -> &[LogEntry] {
846 &self.entries
847 }
848
849 pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
851 &self.entries[range]
852 }
853
854 pub fn last_event(&self) -> Option<&Event> {
856 if self.current_index > 0 {
857 Some(&self.entries[self.current_index - 1].event)
858 } else {
859 None
860 }
861 }
862
863 pub fn clear(&mut self) {
865 self.entries.clear();
866 self.current_index = 0;
867 self.snapshots.clear();
868 }
869
870 pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
872 use std::io::Write;
873 let file = std::fs::File::create(path)?;
874 let mut writer = std::io::BufWriter::new(file);
875
876 for entry in &self.entries {
877 let json = serde_json::to_string(entry)?;
878 writeln!(writer, "{json}")?;
879 }
880
881 Ok(())
882 }
883
884 pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
886 use std::io::BufRead;
887 let file = std::fs::File::open(path)?;
888 let reader = std::io::BufReader::new(file);
889
890 let mut log = Self::new();
891
892 for line in reader.lines() {
893 let line = line?;
894 if line.trim().is_empty() {
895 continue;
896 }
897 let entry: LogEntry = serde_json::from_str(&line)?;
898 log.entries.push(entry);
899 }
900
901 log.current_index = log.entries.len();
902
903 Ok(log)
904 }
905
906 pub fn set_snapshot_interval(&mut self, interval: usize) {
908 self.snapshot_interval = interval;
909 }
910}
911
912impl Default for EventLog {
913 fn default() -> Self {
914 Self::new()
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921
922 #[cfg(test)]
924 mod property_tests {
925 use super::*;
926 use proptest::prelude::*;
927
928 fn arb_event() -> impl Strategy<Value = Event> {
930 prop_oneof![
931 (0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
933 position: pos,
934 text,
935 cursor_id: CursorId(0),
936 }),
937 (0usize..1000, 1usize..50).prop_map(|(pos, len)| Event::Delete {
939 range: pos..pos + len,
940 deleted_text: "x".repeat(len),
941 cursor_id: CursorId(0),
942 }),
943 ]
944 }
945
946 proptest! {
947 #[test]
949 fn event_inverse_property(event in arb_event()) {
950 if let Some(inverse) = event.inverse() {
951 if let Some(double_inverse) = inverse.inverse() {
954 match (&event, &double_inverse) {
955 (Event::Insert { position: p1, text: t1, .. },
956 Event::Insert { position: p2, text: t2, .. }) => {
957 assert_eq!(p1, p2);
958 assert_eq!(t1, t2);
959 }
960 (Event::Delete { range: r1, deleted_text: dt1, .. },
961 Event::Delete { range: r2, deleted_text: dt2, .. }) => {
962 assert_eq!(r1, r2);
963 assert_eq!(dt1, dt2);
964 }
965 _ => {}
966 }
967 }
968 }
969 }
970
971 #[test]
973 fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
974 let mut log = EventLog::new();
975
976 for event in &events {
978 log.append(event.clone());
979 }
980
981 let after_append = log.current_index();
982
983 let mut undo_count = 0;
985 while log.can_undo() {
986 log.undo();
987 undo_count += 1;
988 }
989
990 assert_eq!(log.current_index(), 0);
991 assert_eq!(undo_count, events.len());
992
993 let mut redo_count = 0;
995 while log.can_redo() {
996 log.redo();
997 redo_count += 1;
998 }
999
1000 assert_eq!(log.current_index(), after_append);
1001 assert_eq!(redo_count, events.len());
1002 }
1003
1004 #[test]
1006 fn append_after_undo_truncates(
1007 initial_events in prop::collection::vec(arb_event(), 2..10),
1008 new_event in arb_event()
1009 ) {
1010 let mut log = EventLog::new();
1011
1012 for event in &initial_events {
1013 log.append(event.clone());
1014 }
1015
1016 log.undo();
1018 let index_after_undo = log.current_index();
1019
1020 log.append(new_event);
1022
1023 assert_eq!(log.current_index(), index_after_undo + 1);
1025 assert!(!log.can_redo());
1026 }
1027 }
1028 }
1029
1030 #[test]
1031 fn test_event_log_append() {
1032 let mut log = EventLog::new();
1033 let event = Event::Insert {
1034 position: 0,
1035 text: "hello".to_string(),
1036 cursor_id: CursorId(0),
1037 };
1038
1039 let index = log.append(event);
1040 assert_eq!(index, 0);
1041 assert_eq!(log.current_index(), 1);
1042 assert_eq!(log.entries().len(), 1);
1043 }
1044
1045 #[test]
1046 fn test_undo_redo() {
1047 let mut log = EventLog::new();
1048
1049 log.append(Event::Insert {
1050 position: 0,
1051 text: "a".to_string(),
1052 cursor_id: CursorId(0),
1053 });
1054
1055 log.append(Event::Insert {
1056 position: 1,
1057 text: "b".to_string(),
1058 cursor_id: CursorId(0),
1059 });
1060
1061 assert_eq!(log.current_index(), 2);
1062 assert!(log.can_undo());
1063 assert!(!log.can_redo());
1064
1065 log.undo();
1066 assert_eq!(log.current_index(), 1);
1067 assert!(log.can_undo());
1068 assert!(log.can_redo());
1069
1070 log.undo();
1071 assert_eq!(log.current_index(), 0);
1072 assert!(!log.can_undo());
1073 assert!(log.can_redo());
1074
1075 log.redo();
1076 assert_eq!(log.current_index(), 1);
1077 }
1078
1079 #[test]
1080 fn test_event_inverse() {
1081 let insert = Event::Insert {
1082 position: 5,
1083 text: "hello".to_string(),
1084 cursor_id: CursorId(0),
1085 };
1086
1087 let inverse = insert.inverse().unwrap();
1088 match inverse {
1089 Event::Delete {
1090 range,
1091 deleted_text,
1092 ..
1093 } => {
1094 assert_eq!(range, 5..10);
1095 assert_eq!(deleted_text, "hello");
1096 }
1097 _ => panic!("Expected Delete event"),
1098 }
1099 }
1100
1101 #[test]
1102 fn test_truncate_on_new_event_after_undo() {
1103 let mut log = EventLog::new();
1104
1105 log.append(Event::Insert {
1106 position: 0,
1107 text: "a".to_string(),
1108 cursor_id: CursorId(0),
1109 });
1110
1111 log.append(Event::Insert {
1112 position: 1,
1113 text: "b".to_string(),
1114 cursor_id: CursorId(0),
1115 });
1116
1117 log.undo();
1118 assert_eq!(log.entries().len(), 2);
1119
1120 log.append(Event::Insert {
1122 position: 1,
1123 text: "c".to_string(),
1124 cursor_id: CursorId(0),
1125 });
1126
1127 assert_eq!(log.entries().len(), 2);
1128 assert_eq!(log.current_index(), 2);
1129 }
1130
1131 #[test]
1132 fn test_navigation_after_undo_preserves_redo() {
1133 let mut log = EventLog::new();
1136
1137 log.append(Event::Insert {
1139 position: 0,
1140 text: "a".to_string(),
1141 cursor_id: CursorId(0),
1142 });
1143 log.append(Event::MoveCursor {
1144 cursor_id: CursorId(0),
1145 old_position: 0,
1146 new_position: 1,
1147 old_anchor: None,
1148 new_anchor: None,
1149 old_sticky_column: 0,
1150 new_sticky_column: 0,
1151 });
1152 assert_eq!(log.current_index(), 2);
1153
1154 let undo_events = log.undo();
1156 assert!(!undo_events.is_empty());
1157 assert_eq!(log.current_index(), 0);
1158 assert!(log.can_redo());
1159
1160 log.append(Event::MoveCursor {
1162 cursor_id: CursorId(0),
1163 old_position: 0,
1164 new_position: 0,
1165 old_anchor: None,
1166 new_anchor: None,
1167 old_sticky_column: 0,
1168 new_sticky_column: 0,
1169 });
1170 assert!(
1171 log.can_redo(),
1172 "Navigation after undo should preserve redo history"
1173 );
1174
1175 let redo_events = log.redo();
1177 assert!(
1178 !redo_events.is_empty(),
1179 "Redo should return events after navigation"
1180 );
1181 }
1182
1183 #[test]
1184 fn test_write_action_after_undo_clears_redo() {
1185 let mut log = EventLog::new();
1187
1188 log.append(Event::Insert {
1189 position: 0,
1190 text: "a".to_string(),
1191 cursor_id: CursorId(0),
1192 });
1193
1194 log.undo();
1195 assert!(log.can_redo());
1196
1197 log.append(Event::Insert {
1199 position: 0,
1200 text: "b".to_string(),
1201 cursor_id: CursorId(0),
1202 });
1203 assert!(
1204 !log.can_redo(),
1205 "Write action after undo should clear redo history"
1206 );
1207 }
1208
1209 #[test]
1218 fn test_is_at_saved_position_after_truncate() {
1219 let mut log = EventLog::new();
1220
1221 for i in 0..150 {
1223 log.append(Event::Insert {
1224 position: i,
1225 text: "x".to_string(),
1226 cursor_id: CursorId(0),
1227 });
1228 }
1229
1230 assert_eq!(log.entries().len(), 150);
1231 assert_eq!(log.current_index(), 150);
1232
1233 log.mark_saved();
1235
1236 for _ in 0..30 {
1238 log.undo();
1239 }
1240 assert_eq!(log.current_index(), 120);
1241 assert_eq!(log.entries().len(), 150);
1242
1243 log.append(Event::Insert {
1245 position: 0,
1246 text: "NEW".to_string(),
1247 cursor_id: CursorId(0),
1248 });
1249
1250 assert_eq!(log.entries().len(), 121);
1252 assert_eq!(log.current_index(), 121);
1253
1254 let result = log.is_at_saved_position();
1258
1259 assert!(
1261 !result,
1262 "Should not be at saved position after undo + new edit"
1263 );
1264 }
1265}