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 },
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
346pub enum MarginPositionData {
347 Left,
348 Right,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353pub enum MarginContentData {
354 Text(String),
355 Symbol {
356 text: String,
357 color: Option<(u8, u8, u8)>, },
359 Empty,
360}
361
362impl Event {
363 pub fn inverse(&self) -> Option<Self> {
366 match self {
367 Self::Insert { position, text, .. } => {
368 let range = *position..(position + text.len());
369 Some(Self::Delete {
370 range,
371 deleted_text: text.clone(),
372 cursor_id: CursorId::UNDO_SENTINEL,
373 })
374 }
375 Self::Delete {
376 range,
377 deleted_text,
378 ..
379 } => Some(Self::Insert {
380 position: range.start,
381 text: deleted_text.clone(),
382 cursor_id: CursorId::UNDO_SENTINEL,
383 }),
384 Self::Batch {
385 events,
386 description,
387 } => {
388 let inverted: Option<Vec<Self>> =
390 events.iter().rev().map(|e| e.inverse()).collect();
391
392 inverted.map(|inverted_events| Self::Batch {
393 events: inverted_events,
394 description: format!("Undo: {}", description),
395 })
396 }
397 Self::AddCursor {
398 cursor_id,
399 position,
400 anchor,
401 } => {
402 Some(Self::RemoveCursor {
404 cursor_id: *cursor_id,
405 position: *position,
406 anchor: *anchor,
407 })
408 }
409 Self::RemoveCursor {
410 cursor_id,
411 position,
412 anchor,
413 } => {
414 Some(Self::AddCursor {
416 cursor_id: *cursor_id,
417 position: *position,
418 anchor: *anchor,
419 })
420 }
421 Self::MoveCursor {
422 cursor_id,
423 old_position,
424 new_position,
425 old_anchor,
426 new_anchor,
427 old_sticky_column,
428 new_sticky_column,
429 } => {
430 Some(Self::MoveCursor {
432 cursor_id: *cursor_id,
433 old_position: *new_position,
434 new_position: *old_position,
435 old_anchor: *new_anchor,
436 new_anchor: *old_anchor,
437 old_sticky_column: *new_sticky_column,
438 new_sticky_column: *old_sticky_column,
439 })
440 }
441 Self::AddOverlay { .. } => {
442 None
444 }
445 Self::RemoveOverlay { .. } => {
446 None
448 }
449 Self::ClearNamespace { .. } => {
450 None
452 }
453 Self::Scroll { line_offset } => Some(Self::Scroll {
454 line_offset: -line_offset,
455 }),
456 Self::SetViewport { top_line: _ } => {
457 None
459 }
460 Self::ChangeMode { mode: _ } => {
461 None
463 }
464 Self::BulkEdit {
465 old_snapshot,
466 new_snapshot,
467 old_cursors,
468 new_cursors,
469 description,
470 edits,
471 displaced_markers,
472 } => {
473 let inverted_edits: Vec<(usize, usize, usize)> = edits
476 .iter()
477 .map(|(pos, del_len, ins_len)| (*pos, *ins_len, *del_len))
478 .collect();
479
480 Some(Self::BulkEdit {
481 old_snapshot: new_snapshot.clone(),
482 new_snapshot: old_snapshot.clone(),
483 old_cursors: new_cursors.clone(),
484 new_cursors: old_cursors.clone(),
485 description: format!("Undo: {}", description),
486 edits: inverted_edits,
487 displaced_markers: displaced_markers.clone(),
491 })
492 }
493 _ => None,
495 }
496 }
497
498 pub fn modifies_buffer(&self) -> bool {
500 match self {
501 Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
502 Self::Batch { events, .. } => events.iter().any(|e| e.modifies_buffer()),
503 _ => false,
504 }
505 }
506
507 pub fn is_write_action(&self) -> bool {
520 match self {
521 Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
523
524 Self::AddCursor { .. } | Self::RemoveCursor { .. } => true,
526
527 Self::Batch { events, .. } => events.iter().any(|e| e.is_write_action()),
529
530 _ => false,
532 }
533 }
534
535 pub fn cursor_id(&self) -> Option<CursorId> {
537 match self {
538 Self::Insert { cursor_id, .. }
539 | Self::Delete { cursor_id, .. }
540 | Self::MoveCursor { cursor_id, .. }
541 | Self::AddCursor { cursor_id, .. }
542 | Self::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
543 _ => None,
544 }
545 }
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct LogEntry {
551 pub event: Event,
553
554 pub timestamp: u64,
556
557 pub description: Option<String>,
559
560 #[serde(default, skip_serializing_if = "Vec::is_empty")]
565 pub displaced_markers: Vec<(u64, usize)>,
566}
567
568impl LogEntry {
569 pub fn new(event: Event) -> Self {
570 Self {
571 event,
572 timestamp: std::time::SystemTime::now()
573 .duration_since(std::time::UNIX_EPOCH)
574 .unwrap()
575 .as_millis() as u64,
576 description: None,
577 displaced_markers: Vec::new(),
578 }
579 }
580
581 pub fn with_description(mut self, description: String) -> Self {
582 self.description = Some(description);
583 self
584 }
585}
586
587#[derive(Debug, Clone)]
589pub struct Snapshot {
590 pub log_index: usize,
592
593 pub buffer_state: (),
596
597 pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
599}
600
601pub struct EventLog {
603 entries: Vec<LogEntry>,
605
606 current_index: usize,
608
609 snapshots: Vec<Snapshot>,
611
612 snapshot_interval: usize,
614
615 #[cfg(feature = "runtime")]
617 stream_file: Option<std::fs::File>,
618
619 saved_at_index: Option<usize>,
622}
623
624impl EventLog {
625 pub fn new() -> Self {
627 Self {
628 entries: Vec::new(),
629 current_index: 0,
630 snapshots: Vec::new(),
631 snapshot_interval: 100,
632 #[cfg(feature = "runtime")]
633 stream_file: None,
634 saved_at_index: Some(0), }
636 }
637
638 pub fn mark_saved(&mut self) {
641 self.saved_at_index = Some(self.current_index);
642 }
643
644 pub fn clear_saved_position(&mut self) {
648 self.saved_at_index = None;
649 }
650
651 pub fn is_at_saved_position(&self) -> bool {
655 match self.saved_at_index {
656 None => false,
657 Some(saved_idx) if saved_idx == self.current_index => true,
658 Some(saved_idx) => {
659 let (start, end) = if saved_idx < self.current_index {
662 (saved_idx, self.current_index)
663 } else {
664 (self.current_index, saved_idx)
665 };
666
667 self.entries[start..end]
669 .iter()
670 .all(|entry| !entry.event.modifies_buffer())
671 }
672 }
673 }
674
675 #[cfg(feature = "runtime")]
677 pub fn enable_streaming<P: AsRef<std::path::Path>>(&mut self, path: P) -> std::io::Result<()> {
678 use std::io::Write;
679
680 let mut file = std::fs::OpenOptions::new()
681 .create(true)
682 .write(true)
683 .truncate(true)
684 .open(path)?;
685
686 writeln!(file, "# Event Log Stream")?;
688 writeln!(file, "# Started at: {}", chrono::Local::now())?;
689 writeln!(file, "# Format: JSON Lines (one event per line)")?;
690 writeln!(file, "#")?;
691
692 self.stream_file = Some(file);
693 Ok(())
694 }
695
696 #[cfg(feature = "runtime")]
698 pub fn disable_streaming(&mut self) {
699 self.stream_file = None;
700 }
701
702 #[cfg(feature = "runtime")]
704 pub fn log_render_state(
705 &mut self,
706 cursor_pos: usize,
707 screen_cursor_x: u16,
708 screen_cursor_y: u16,
709 buffer_len: usize,
710 ) {
711 if let Some(ref mut file) = self.stream_file {
712 use std::io::Write;
713
714 let render_info = serde_json::json!({
715 "type": "render",
716 "timestamp": chrono::Local::now().to_rfc3339(),
717 "cursor_position": cursor_pos,
718 "screen_cursor": {"x": screen_cursor_x, "y": screen_cursor_y},
719 "buffer_length": buffer_len,
720 });
721
722 if let Err(e) = writeln!(file, "{render_info}") {
723 tracing::trace!("Warning: Failed to write render info to stream: {e}");
724 }
725 if let Err(e) = file.flush() {
726 tracing::trace!("Warning: Failed to flush event stream: {e}");
727 }
728 }
729 }
730
731 #[cfg(feature = "runtime")]
733 pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
734 if let Some(ref mut file) = self.stream_file {
735 use std::io::Write;
736
737 let keystroke_info = serde_json::json!({
738 "type": "keystroke",
739 "timestamp": chrono::Local::now().to_rfc3339(),
740 "key": key_code,
741 "modifiers": modifiers,
742 });
743
744 if let Err(e) = writeln!(file, "{keystroke_info}") {
745 tracing::trace!("Warning: Failed to write keystroke to stream: {e}");
746 }
747 if let Err(e) = file.flush() {
748 tracing::trace!("Warning: Failed to flush event stream: {e}");
749 }
750 }
751 }
752
753 pub fn append(&mut self, event: Event) -> usize {
755 if self.current_index < self.entries.len() {
761 if event.is_write_action() {
762 self.entries.truncate(self.current_index);
764
765 if let Some(saved_idx) = self.saved_at_index {
767 if saved_idx > self.current_index {
768 self.saved_at_index = None;
769 }
770 }
771 } else {
772 return self.current_index;
774 }
775 }
776
777 #[cfg(feature = "runtime")]
779 if let Some(ref mut file) = self.stream_file {
780 use std::io::Write;
781
782 let stream_entry = serde_json::json!({
783 "index": self.entries.len(),
784 "timestamp": chrono::Local::now().to_rfc3339(),
785 "event": event,
786 });
787
788 if let Err(e) = writeln!(file, "{stream_entry}") {
790 tracing::trace!("Warning: Failed to write to event stream: {e}");
791 }
792 if let Err(e) = file.flush() {
793 tracing::trace!("Warning: Failed to flush event stream: {e}");
794 }
795 }
796
797 let entry = LogEntry::new(event);
798 self.entries.push(entry);
799 self.current_index = self.entries.len();
800
801 if self.entries.len().is_multiple_of(self.snapshot_interval) {
803 }
806
807 self.current_index - 1
808 }
809
810 pub fn set_displaced_markers_on_last(&mut self, markers: Vec<(u64, usize)>) {
814 if let Some(entry) = self.entries.last_mut() {
815 entry.displaced_markers = markers;
816 }
817 }
818
819 pub fn current_index(&self) -> usize {
821 self.current_index
822 }
823
824 pub fn len(&self) -> usize {
826 self.entries.len()
827 }
828
829 pub fn is_empty(&self) -> bool {
831 self.entries.is_empty()
832 }
833
834 pub fn can_undo(&self) -> bool {
836 self.current_index > 0
837 }
838
839 pub fn can_redo(&self) -> bool {
841 self.current_index < self.entries.len()
842 }
843
844 pub fn undo(&mut self) -> Vec<(Event, Vec<(u64, usize)>)> {
850 let mut inverse_events = Vec::new();
851 let mut found_write_action = false;
852
853 while self.can_undo() && !found_write_action {
855 self.current_index -= 1;
856 let entry = &self.entries[self.current_index];
857
858 if entry.event.is_write_action() {
860 found_write_action = true;
861 }
862
863 if let Some(inverse) = entry.event.inverse() {
865 inverse_events.push((inverse, entry.displaced_markers.clone()));
866 }
867 }
869
870 inverse_events
871 }
872
873 pub fn redo(&mut self) -> Vec<Event> {
877 let mut events = Vec::new();
878 let mut found_write_action = false;
879
880 while self.can_redo() {
882 let event = self.entries[self.current_index].event.clone();
883
884 if found_write_action && event.is_write_action() {
886 break;
888 }
889
890 self.current_index += 1;
891
892 if event.is_write_action() {
894 found_write_action = true;
895 }
896
897 events.push(event);
898 }
899
900 events
901 }
902
903 pub fn entries(&self) -> &[LogEntry] {
905 &self.entries
906 }
907
908 pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
910 &self.entries[range]
911 }
912
913 pub fn last_event(&self) -> Option<&Event> {
915 if self.current_index > 0 {
916 Some(&self.entries[self.current_index - 1].event)
917 } else {
918 None
919 }
920 }
921
922 pub fn clear(&mut self) {
924 self.entries.clear();
925 self.current_index = 0;
926 self.snapshots.clear();
927 }
928
929 pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
931 use std::io::Write;
932 let file = std::fs::File::create(path)?;
933 let mut writer = std::io::BufWriter::new(file);
934
935 for entry in &self.entries {
936 let json = serde_json::to_string(entry)?;
937 writeln!(writer, "{json}")?;
938 }
939
940 Ok(())
941 }
942
943 pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
945 use std::io::BufRead;
946 let file = std::fs::File::open(path)?;
947 let reader = std::io::BufReader::new(file);
948
949 let mut log = Self::new();
950
951 for line in reader.lines() {
952 let line = line?;
953 if line.trim().is_empty() {
954 continue;
955 }
956 let entry: LogEntry = serde_json::from_str(&line)?;
957 log.entries.push(entry);
958 }
959
960 log.current_index = log.entries.len();
961
962 Ok(log)
963 }
964
965 pub fn set_snapshot_interval(&mut self, interval: usize) {
967 self.snapshot_interval = interval;
968 }
969}
970
971impl Default for EventLog {
972 fn default() -> Self {
973 Self::new()
974 }
975}
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980
981 #[cfg(test)]
983 mod property_tests {
984 use super::*;
985 use proptest::prelude::*;
986
987 fn arb_event() -> impl Strategy<Value = Event> {
989 prop_oneof![
990 (0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
992 position: pos,
993 text,
994 cursor_id: CursorId(0),
995 }),
996 (0usize..1000, 1usize..50).prop_map(|(pos, len)| Event::Delete {
998 range: pos..pos + len,
999 deleted_text: "x".repeat(len),
1000 cursor_id: CursorId(0),
1001 }),
1002 ]
1003 }
1004
1005 proptest! {
1006 #[test]
1008 fn event_inverse_property(event in arb_event()) {
1009 if let Some(inverse) = event.inverse() {
1010 if let Some(double_inverse) = inverse.inverse() {
1013 match (&event, &double_inverse) {
1014 (Event::Insert { position: p1, text: t1, .. },
1015 Event::Insert { position: p2, text: t2, .. }) => {
1016 assert_eq!(p1, p2);
1017 assert_eq!(t1, t2);
1018 }
1019 (Event::Delete { range: r1, deleted_text: dt1, .. },
1020 Event::Delete { range: r2, deleted_text: dt2, .. }) => {
1021 assert_eq!(r1, r2);
1022 assert_eq!(dt1, dt2);
1023 }
1024 _ => {}
1025 }
1026 }
1027 }
1028 }
1029
1030 #[test]
1032 fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
1033 let mut log = EventLog::new();
1034
1035 for event in &events {
1037 log.append(event.clone());
1038 }
1039
1040 let after_append = log.current_index();
1041
1042 let mut undo_count = 0;
1044 while log.can_undo() {
1045 log.undo();
1046 undo_count += 1;
1047 }
1048
1049 assert_eq!(log.current_index(), 0);
1050 assert_eq!(undo_count, events.len());
1051
1052 let mut redo_count = 0;
1054 while log.can_redo() {
1055 log.redo();
1056 redo_count += 1;
1057 }
1058
1059 assert_eq!(log.current_index(), after_append);
1060 assert_eq!(redo_count, events.len());
1061 }
1062
1063 #[test]
1065 fn append_after_undo_truncates(
1066 initial_events in prop::collection::vec(arb_event(), 2..10),
1067 new_event in arb_event()
1068 ) {
1069 let mut log = EventLog::new();
1070
1071 for event in &initial_events {
1072 log.append(event.clone());
1073 }
1074
1075 log.undo();
1077 let index_after_undo = log.current_index();
1078
1079 log.append(new_event);
1081
1082 assert_eq!(log.current_index(), index_after_undo + 1);
1084 assert!(!log.can_redo());
1085 }
1086 }
1087 }
1088
1089 #[test]
1090 fn test_event_log_append() {
1091 let mut log = EventLog::new();
1092 let event = Event::Insert {
1093 position: 0,
1094 text: "hello".to_string(),
1095 cursor_id: CursorId(0),
1096 };
1097
1098 let index = log.append(event);
1099 assert_eq!(index, 0);
1100 assert_eq!(log.current_index(), 1);
1101 assert_eq!(log.entries().len(), 1);
1102 }
1103
1104 #[test]
1105 fn test_undo_redo() {
1106 let mut log = EventLog::new();
1107
1108 log.append(Event::Insert {
1109 position: 0,
1110 text: "a".to_string(),
1111 cursor_id: CursorId(0),
1112 });
1113
1114 log.append(Event::Insert {
1115 position: 1,
1116 text: "b".to_string(),
1117 cursor_id: CursorId(0),
1118 });
1119
1120 assert_eq!(log.current_index(), 2);
1121 assert!(log.can_undo());
1122 assert!(!log.can_redo());
1123
1124 log.undo();
1125 assert_eq!(log.current_index(), 1);
1126 assert!(log.can_undo());
1127 assert!(log.can_redo());
1128
1129 log.undo();
1130 assert_eq!(log.current_index(), 0);
1131 assert!(!log.can_undo());
1132 assert!(log.can_redo());
1133
1134 log.redo();
1135 assert_eq!(log.current_index(), 1);
1136 }
1137
1138 #[test]
1139 fn test_event_inverse() {
1140 let insert = Event::Insert {
1141 position: 5,
1142 text: "hello".to_string(),
1143 cursor_id: CursorId(0),
1144 };
1145
1146 let inverse = insert.inverse().unwrap();
1147 match inverse {
1148 Event::Delete {
1149 range,
1150 deleted_text,
1151 ..
1152 } => {
1153 assert_eq!(range, 5..10);
1154 assert_eq!(deleted_text, "hello");
1155 }
1156 _ => panic!("Expected Delete event"),
1157 }
1158 }
1159
1160 #[test]
1161 fn test_truncate_on_new_event_after_undo() {
1162 let mut log = EventLog::new();
1163
1164 log.append(Event::Insert {
1165 position: 0,
1166 text: "a".to_string(),
1167 cursor_id: CursorId(0),
1168 });
1169
1170 log.append(Event::Insert {
1171 position: 1,
1172 text: "b".to_string(),
1173 cursor_id: CursorId(0),
1174 });
1175
1176 log.undo();
1177 assert_eq!(log.entries().len(), 2);
1178
1179 log.append(Event::Insert {
1181 position: 1,
1182 text: "c".to_string(),
1183 cursor_id: CursorId(0),
1184 });
1185
1186 assert_eq!(log.entries().len(), 2);
1187 assert_eq!(log.current_index(), 2);
1188 }
1189
1190 #[test]
1191 fn test_navigation_after_undo_preserves_redo() {
1192 let mut log = EventLog::new();
1195
1196 log.append(Event::Insert {
1198 position: 0,
1199 text: "a".to_string(),
1200 cursor_id: CursorId(0),
1201 });
1202 log.append(Event::MoveCursor {
1203 cursor_id: CursorId(0),
1204 old_position: 0,
1205 new_position: 1,
1206 old_anchor: None,
1207 new_anchor: None,
1208 old_sticky_column: 0,
1209 new_sticky_column: 0,
1210 });
1211 assert_eq!(log.current_index(), 2);
1212
1213 let undo_events = log.undo();
1215 assert!(!undo_events.is_empty());
1216 assert_eq!(log.current_index(), 0);
1217 assert!(log.can_redo());
1218
1219 log.append(Event::MoveCursor {
1221 cursor_id: CursorId(0),
1222 old_position: 0,
1223 new_position: 0,
1224 old_anchor: None,
1225 new_anchor: None,
1226 old_sticky_column: 0,
1227 new_sticky_column: 0,
1228 });
1229 assert!(
1230 log.can_redo(),
1231 "Navigation after undo should preserve redo history"
1232 );
1233
1234 let redo_events = log.redo();
1236 assert!(
1237 !redo_events.is_empty(),
1238 "Redo should return events after navigation"
1239 );
1240 }
1241
1242 #[test]
1243 fn test_write_action_after_undo_clears_redo() {
1244 let mut log = EventLog::new();
1246
1247 log.append(Event::Insert {
1248 position: 0,
1249 text: "a".to_string(),
1250 cursor_id: CursorId(0),
1251 });
1252
1253 log.undo();
1254 assert!(log.can_redo());
1255
1256 log.append(Event::Insert {
1258 position: 0,
1259 text: "b".to_string(),
1260 cursor_id: CursorId(0),
1261 });
1262 assert!(
1263 !log.can_redo(),
1264 "Write action after undo should clear redo history"
1265 );
1266 }
1267
1268 #[test]
1277 fn test_is_at_saved_position_after_truncate() {
1278 let mut log = EventLog::new();
1279
1280 for i in 0..150 {
1282 log.append(Event::Insert {
1283 position: i,
1284 text: "x".to_string(),
1285 cursor_id: CursorId(0),
1286 });
1287 }
1288
1289 assert_eq!(log.entries().len(), 150);
1290 assert_eq!(log.current_index(), 150);
1291
1292 log.mark_saved();
1294
1295 for _ in 0..30 {
1297 log.undo();
1298 }
1299 assert_eq!(log.current_index(), 120);
1300 assert_eq!(log.entries().len(), 150);
1301
1302 log.append(Event::Insert {
1304 position: 0,
1305 text: "NEW".to_string(),
1306 cursor_id: CursorId(0),
1307 });
1308
1309 assert_eq!(log.entries().len(), 121);
1311 assert_eq!(log.current_index(), 121);
1312
1313 let result = log.is_at_saved_position();
1317
1318 assert!(
1320 !result,
1321 "Should not be at saved position after undo + new edit"
1322 );
1323 }
1324}