Skip to main content

fresh/model/
event.rs

1use crate::model::piece_tree::PieceTree;
2pub use fresh_core::api::{OverlayColorSpec, OverlayOptions};
3pub use fresh_core::overlay::{OverlayHandle, OverlayNamespace};
4pub use fresh_core::{BufferId, CursorId, SplitDirection, SplitId};
5use serde::{Deserialize, Serialize};
6use std::ops::Range;
7use std::sync::Arc;
8
9/// Core event types representing all possible state changes
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub enum Event {
12    /// Insert text at a position
13    Insert {
14        position: usize,
15        text: String,
16        cursor_id: CursorId,
17    },
18
19    /// Delete a range of text
20    Delete {
21        range: Range<usize>,
22        deleted_text: String,
23        cursor_id: CursorId,
24    },
25
26    /// Move a cursor to a new position
27    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    /// Add a new cursor
38    AddCursor {
39        cursor_id: CursorId,
40        position: usize,
41        anchor: Option<usize>,
42    },
43
44    /// Remove a cursor (stores cursor state for undo)
45    RemoveCursor {
46        cursor_id: CursorId,
47        position: usize,
48        anchor: Option<usize>,
49    },
50
51    /// Scroll the viewport
52    Scroll {
53        line_offset: isize,
54    },
55
56    /// Set viewport to specific position
57    SetViewport {
58        top_line: usize,
59    },
60
61    /// Center the viewport on the cursor
62    Recenter,
63
64    /// Set the anchor (selection start) for a cursor
65    SetAnchor {
66        cursor_id: CursorId,
67        position: usize,
68    },
69
70    /// Clear the anchor and reset deselect_on_move for a cursor
71    /// Used to cancel Emacs mark mode
72    ClearAnchor {
73        cursor_id: CursorId,
74    },
75
76    /// Change mode (if implementing modal editing)
77    ChangeMode {
78        mode: String,
79    },
80
81    /// Add an overlay (for decorations like underlines, highlights)
82    AddOverlay {
83        namespace: Option<OverlayNamespace>,
84        range: Range<usize>,
85        face: OverlayFace,
86        priority: i32,
87        message: Option<String>,
88        /// Whether to extend the overlay's background to the end of the visual line
89        extend_to_line_end: bool,
90    },
91
92    /// Remove overlay by handle
93    RemoveOverlay {
94        handle: OverlayHandle,
95    },
96
97    /// Remove all overlays in a range
98    RemoveOverlaysInRange {
99        range: Range<usize>,
100    },
101
102    /// Clear all overlays in a namespace
103    ClearNamespace {
104        namespace: OverlayNamespace,
105    },
106
107    /// Clear all overlays
108    ClearOverlays,
109
110    /// Show a popup
111    ShowPopup {
112        popup: PopupData,
113    },
114
115    /// Hide the topmost popup
116    HidePopup,
117
118    /// Clear all popups
119    ClearPopups,
120
121    /// Navigate popup selection (for list popups)
122    PopupSelectNext,
123    PopupSelectPrev,
124    PopupPageDown,
125    PopupPageUp,
126
127    /// Margin events
128    /// Add a margin annotation
129    AddMarginAnnotation {
130        line: usize,
131        position: MarginPositionData,
132        content: MarginContentData,
133        annotation_id: Option<String>,
134    },
135
136    /// Remove margin annotation by ID
137    RemoveMarginAnnotation {
138        annotation_id: String,
139    },
140
141    /// Remove all margin annotations at a specific line
142    RemoveMarginAnnotationsAtLine {
143        line: usize,
144        position: MarginPositionData,
145    },
146
147    /// Clear all margin annotations in a position
148    ClearMarginPosition {
149        position: MarginPositionData,
150    },
151
152    /// Clear all margin annotations
153    ClearMargins,
154
155    /// Enable/disable line numbers
156    SetLineNumbers {
157        enabled: bool,
158    },
159
160    /// Split view events
161    /// Split the active pane
162    SplitPane {
163        direction: SplitDirection,
164        new_buffer_id: BufferId,
165        ratio: f32,
166    },
167
168    /// Close a split pane
169    CloseSplit {
170        split_id: SplitId,
171    },
172
173    /// Set the active split pane
174    SetActiveSplit {
175        split_id: SplitId,
176    },
177
178    /// Adjust the split ratio
179    AdjustSplitRatio {
180        split_id: SplitId,
181        delta: f32,
182    },
183
184    /// Navigate to next split
185    NextSplit,
186
187    /// Navigate to previous split
188    PrevSplit,
189
190    /// Batch of events that should be undone/redone atomically
191    /// Used for multi-cursor operations where all cursors perform the same action
192    Batch {
193        events: Vec<Event>,
194        description: String,
195    },
196
197    /// Efficient bulk edit that stores tree snapshots for O(1) undo/redo
198    /// Used for multi-cursor operations, toggle comment, indent/dedent, etc.
199    /// This avoids O(n²) complexity by applying all edits in a single tree pass.
200    ///
201    /// Key insight: PieceTree uses Arc<PieceTreeNode> (persistent data structure),
202    /// so storing trees for undo/redo is O(1) (Arc clone), not O(n) (content copy).
203    BulkEdit {
204        /// Tree state before the edit (for undo)
205        #[serde(skip)]
206        old_tree: Option<Arc<PieceTree>>,
207        /// Tree state after the edit (for redo)
208        #[serde(skip)]
209        new_tree: Option<Arc<PieceTree>>,
210        /// Cursor states before the edit
211        old_cursors: Vec<(CursorId, usize, Option<usize>)>,
212        /// Cursor states after the edit
213        new_cursors: Vec<(CursorId, usize, Option<usize>)>,
214        /// Human-readable description
215        description: String,
216    },
217}
218
219/// Overlay face data for events (must be serializable)
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub enum OverlayFace {
222    Underline {
223        color: (u8, u8, u8), // RGB color
224        style: UnderlineStyle,
225    },
226    Background {
227        color: (u8, u8, u8),
228    },
229    Foreground {
230        color: (u8, u8, u8),
231    },
232    /// Full style with theme-aware colors
233    ///
234    /// Uses OverlayOptions which supports both RGB colors and theme keys.
235    /// Theme keys are resolved at render time.
236    Style {
237        options: OverlayOptions,
238    },
239}
240
241impl OverlayFace {
242    /// Create an OverlayFace from OverlayOptions
243    pub fn from_options(options: OverlayOptions) -> Self {
244        OverlayFace::Style { options }
245    }
246}
247
248/// Underline style for overlays
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub enum UnderlineStyle {
251    Straight,
252    Wavy,
253    Dotted,
254    Dashed,
255}
256
257/// Popup data for events (must be serializable)
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct PopupData {
260    pub title: Option<String>,
261    /// Optional description text shown above the content
262    #[serde(default)]
263    pub description: Option<String>,
264    #[serde(default)]
265    pub transient: bool,
266    pub content: PopupContentData,
267    pub position: PopupPositionData,
268    pub width: u16,
269    pub max_height: u16,
270    pub bordered: bool,
271}
272
273/// Popup content for events
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub enum PopupContentData {
276    Text(Vec<String>),
277    List {
278        items: Vec<PopupListItemData>,
279        selected: usize,
280    },
281}
282
283/// Popup list item for events
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct PopupListItemData {
286    pub text: String,
287    pub detail: Option<String>,
288    pub icon: Option<String>,
289    pub data: Option<String>,
290}
291
292/// Popup position for events
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub enum PopupPositionData {
295    AtCursor,
296    BelowCursor,
297    AboveCursor,
298    Fixed { x: u16, y: u16 },
299    Centered,
300    BottomRight,
301}
302
303/// Margin position for events
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
305pub enum MarginPositionData {
306    Left,
307    Right,
308}
309
310/// Margin content for events
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub enum MarginContentData {
313    Text(String),
314    Symbol {
315        text: String,
316        color: Option<(u8, u8, u8)>, // RGB color
317    },
318    Empty,
319}
320
321impl Event {
322    /// Returns the inverse event for undo functionality
323    /// Uses UNDO_SENTINEL cursor_id to avoid moving the cursor during undo
324    pub fn inverse(&self) -> Option<Self> {
325        match self {
326            Self::Insert { position, text, .. } => {
327                let range = *position..(position + text.len());
328                Some(Self::Delete {
329                    range,
330                    deleted_text: text.clone(),
331                    cursor_id: CursorId::UNDO_SENTINEL,
332                })
333            }
334            Self::Delete {
335                range,
336                deleted_text,
337                ..
338            } => Some(Self::Insert {
339                position: range.start,
340                text: deleted_text.clone(),
341                cursor_id: CursorId::UNDO_SENTINEL,
342            }),
343            Self::Batch {
344                events,
345                description,
346            } => {
347                // Invert all events in the batch in reverse order
348                let inverted: Option<Vec<Self>> =
349                    events.iter().rev().map(|e| e.inverse()).collect();
350
351                inverted.map(|inverted_events| Self::Batch {
352                    events: inverted_events,
353                    description: format!("Undo: {}", description),
354                })
355            }
356            Self::AddCursor {
357                cursor_id,
358                position,
359                anchor,
360            } => {
361                // To undo adding a cursor, we remove it (store its state for redo)
362                Some(Self::RemoveCursor {
363                    cursor_id: *cursor_id,
364                    position: *position,
365                    anchor: *anchor,
366                })
367            }
368            Self::RemoveCursor {
369                cursor_id,
370                position,
371                anchor,
372            } => {
373                // To undo removing a cursor, we add it back
374                Some(Self::AddCursor {
375                    cursor_id: *cursor_id,
376                    position: *position,
377                    anchor: *anchor,
378                })
379            }
380            Self::MoveCursor {
381                cursor_id,
382                old_position,
383                new_position,
384                old_anchor,
385                new_anchor,
386                old_sticky_column,
387                new_sticky_column,
388            } => {
389                // Invert by swapping old and new positions
390                Some(Self::MoveCursor {
391                    cursor_id: *cursor_id,
392                    old_position: *new_position,
393                    new_position: *old_position,
394                    old_anchor: *new_anchor,
395                    new_anchor: *old_anchor,
396                    old_sticky_column: *new_sticky_column,
397                    new_sticky_column: *old_sticky_column,
398                })
399            }
400            Self::AddOverlay { .. } => {
401                // Overlays are ephemeral decorations, not undoable
402                None
403            }
404            Self::RemoveOverlay { .. } => {
405                // Overlays are ephemeral decorations, not undoable
406                None
407            }
408            Self::ClearNamespace { .. } => {
409                // Overlays are ephemeral decorations, not undoable
410                None
411            }
412            Self::Scroll { line_offset } => Some(Self::Scroll {
413                line_offset: -line_offset,
414            }),
415            Self::SetViewport { top_line: _ } => {
416                // Can't invert without knowing old top_line
417                None
418            }
419            Self::ChangeMode { mode: _ } => {
420                // Can't invert without knowing old mode
421                None
422            }
423            Self::BulkEdit {
424                old_tree,
425                new_tree,
426                old_cursors,
427                new_cursors,
428                description,
429            } => {
430                // Inverse swaps both trees and cursor states
431                // For undo: old becomes new, new becomes old
432                Some(Self::BulkEdit {
433                    old_tree: new_tree.clone(),
434                    new_tree: old_tree.clone(),
435                    old_cursors: new_cursors.clone(),
436                    new_cursors: old_cursors.clone(),
437                    description: format!("Undo: {}", description),
438                })
439            }
440            // Other events (popups, margins, splits, etc.) are not automatically invertible
441            _ => None,
442        }
443    }
444
445    /// Returns true if this event modifies the buffer content
446    pub fn modifies_buffer(&self) -> bool {
447        match self {
448            Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
449            Self::Batch { events, .. } => events.iter().any(|e| e.modifies_buffer()),
450            _ => false,
451        }
452    }
453
454    /// Returns true if this event is a write action (modifies state in a way that should be undoable)
455    /// Returns false for readonly actions like cursor movement, scrolling, viewport changes, etc.
456    ///
457    /// Write actions include:
458    /// - Buffer modifications (Insert, Delete)
459    /// - Cursor structure changes (AddCursor, RemoveCursor)
460    /// - Batches containing write actions
461    ///
462    /// Readonly actions include:
463    /// - Cursor movement (MoveCursor)
464    /// - Scrolling and viewport changes (Scroll, SetViewport)
465    /// - UI events (overlays, popups, margins, mode changes, etc.)
466    pub fn is_write_action(&self) -> bool {
467        match self {
468            // Buffer modifications are write actions
469            Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
470
471            // Adding/removing cursors are write actions (structural changes)
472            Self::AddCursor { .. } | Self::RemoveCursor { .. } => true,
473
474            // Batches are write actions if they contain any write actions
475            Self::Batch { events, .. } => events.iter().any(|e| e.is_write_action()),
476
477            // All other events are readonly (movement, scrolling, UI, etc.)
478            _ => false,
479        }
480    }
481
482    /// Returns the cursor ID associated with this event, if any
483    pub fn cursor_id(&self) -> Option<CursorId> {
484        match self {
485            Self::Insert { cursor_id, .. }
486            | Self::Delete { cursor_id, .. }
487            | Self::MoveCursor { cursor_id, .. }
488            | Self::AddCursor { cursor_id, .. }
489            | Self::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
490            _ => None,
491        }
492    }
493}
494
495/// A log entry containing an event and metadata
496#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct LogEntry {
498    /// The event
499    pub event: Event,
500
501    /// Timestamp when the event occurred (milliseconds since epoch)
502    pub timestamp: u64,
503
504    /// Optional description for debugging
505    pub description: Option<String>,
506}
507
508impl LogEntry {
509    pub fn new(event: Event) -> Self {
510        Self {
511            event,
512            timestamp: std::time::SystemTime::now()
513                .duration_since(std::time::UNIX_EPOCH)
514                .unwrap()
515                .as_millis() as u64,
516            description: None,
517        }
518    }
519
520    pub fn with_description(mut self, description: String) -> Self {
521        self.description = Some(description);
522        self
523    }
524}
525
526/// Snapshot of editor state for fast undo/redo
527#[derive(Debug, Clone)]
528pub struct Snapshot {
529    /// Index in the event log where this snapshot was taken
530    pub log_index: usize,
531
532    /// Buffer content at this point (stored as ChunkTree reference)
533    /// For now we'll use a placeholder - will be filled in when we implement Buffer
534    pub buffer_state: (),
535
536    /// Cursor positions at this point
537    pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
538}
539
540/// The event log - append-only log of all events
541pub struct EventLog {
542    /// All logged events
543    entries: Vec<LogEntry>,
544
545    /// Current position in the log (for undo/redo)
546    current_index: usize,
547
548    /// Periodic snapshots for fast seeking
549    snapshots: Vec<Snapshot>,
550
551    /// How often to create snapshots (every N events)
552    snapshot_interval: usize,
553
554    /// Optional file for streaming events to disk (runtime only)
555    #[cfg(feature = "runtime")]
556    stream_file: Option<std::fs::File>,
557
558    /// Index at which the buffer was last saved (for tracking modified status)
559    /// When current_index equals saved_at_index, the buffer is not modified
560    saved_at_index: Option<usize>,
561}
562
563impl EventLog {
564    /// Create a new empty event log
565    pub fn new() -> Self {
566        Self {
567            entries: Vec::new(),
568            current_index: 0,
569            snapshots: Vec::new(),
570            snapshot_interval: 100,
571            #[cfg(feature = "runtime")]
572            stream_file: None,
573            saved_at_index: Some(0), // New buffer starts at "saved" state (index 0)
574        }
575    }
576
577    /// Mark the current position as the saved point
578    /// Call this when the buffer is saved to disk
579    pub fn mark_saved(&mut self) {
580        self.saved_at_index = Some(self.current_index);
581    }
582
583    /// Check if the buffer is at the saved position (not modified)
584    /// Returns true if we're at the saved position OR if all events between
585    /// saved_at_index and current_index are readonly (don't modify buffer content)
586    pub fn is_at_saved_position(&self) -> bool {
587        match self.saved_at_index {
588            None => false,
589            Some(saved_idx) if saved_idx == self.current_index => true,
590            Some(saved_idx) => {
591                // Check if all events between saved position and current position
592                // are readonly (don't modify buffer content)
593                let (start, end) = if saved_idx < self.current_index {
594                    (saved_idx, self.current_index)
595                } else {
596                    (self.current_index, saved_idx)
597                };
598
599                // All events in range [start, end) must be readonly
600                self.entries[start..end]
601                    .iter()
602                    .all(|entry| !entry.event.modifies_buffer())
603            }
604        }
605    }
606
607    /// Enable streaming events to a file (runtime only)
608    #[cfg(feature = "runtime")]
609    pub fn enable_streaming<P: AsRef<std::path::Path>>(&mut self, path: P) -> std::io::Result<()> {
610        use std::io::Write;
611
612        let mut file = std::fs::OpenOptions::new()
613            .create(true)
614            .write(true)
615            .truncate(true)
616            .open(path)?;
617
618        // Write header
619        writeln!(file, "# Event Log Stream")?;
620        writeln!(file, "# Started at: {}", chrono::Local::now())?;
621        writeln!(file, "# Format: JSON Lines (one event per line)")?;
622        writeln!(file, "#")?;
623
624        self.stream_file = Some(file);
625        Ok(())
626    }
627
628    /// Disable streaming (runtime only)
629    #[cfg(feature = "runtime")]
630    pub fn disable_streaming(&mut self) {
631        self.stream_file = None;
632    }
633
634    /// Log rendering state (for debugging, runtime only)
635    #[cfg(feature = "runtime")]
636    pub fn log_render_state(
637        &mut self,
638        cursor_pos: usize,
639        screen_cursor_x: u16,
640        screen_cursor_y: u16,
641        buffer_len: usize,
642    ) {
643        if let Some(ref mut file) = self.stream_file {
644            use std::io::Write;
645
646            let render_info = serde_json::json!({
647                "type": "render",
648                "timestamp": chrono::Local::now().to_rfc3339(),
649                "cursor_position": cursor_pos,
650                "screen_cursor": {"x": screen_cursor_x, "y": screen_cursor_y},
651                "buffer_length": buffer_len,
652            });
653
654            if let Err(e) = writeln!(file, "{render_info}") {
655                tracing::trace!("Warning: Failed to write render info to stream: {e}");
656            }
657            if let Err(e) = file.flush() {
658                tracing::trace!("Warning: Failed to flush event stream: {e}");
659            }
660        }
661    }
662
663    /// Log keystroke (for debugging, runtime only)
664    #[cfg(feature = "runtime")]
665    pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
666        if let Some(ref mut file) = self.stream_file {
667            use std::io::Write;
668
669            let keystroke_info = serde_json::json!({
670                "type": "keystroke",
671                "timestamp": chrono::Local::now().to_rfc3339(),
672                "key": key_code,
673                "modifiers": modifiers,
674            });
675
676            if let Err(e) = writeln!(file, "{keystroke_info}") {
677                tracing::trace!("Warning: Failed to write keystroke to stream: {e}");
678            }
679            if let Err(e) = file.flush() {
680                tracing::trace!("Warning: Failed to flush event stream: {e}");
681            }
682        }
683    }
684
685    /// Append an event to the log
686    pub fn append(&mut self, event: Event) -> usize {
687        // If we're not at the end, truncate future events
688        if self.current_index < self.entries.len() {
689            self.entries.truncate(self.current_index);
690
691            // Invalidate saved_at_index if it pointed to a truncated entry
692            if let Some(saved_idx) = self.saved_at_index {
693                if saved_idx > self.current_index {
694                    self.saved_at_index = None;
695                }
696            }
697        }
698
699        // Stream event to file if enabled (runtime only)
700        #[cfg(feature = "runtime")]
701        if let Some(ref mut file) = self.stream_file {
702            use std::io::Write;
703
704            let stream_entry = serde_json::json!({
705                "index": self.entries.len(),
706                "timestamp": chrono::Local::now().to_rfc3339(),
707                "event": event,
708            });
709
710            // Write JSON line and flush immediately for real-time logging
711            if let Err(e) = writeln!(file, "{stream_entry}") {
712                tracing::trace!("Warning: Failed to write to event stream: {e}");
713            }
714            if let Err(e) = file.flush() {
715                tracing::trace!("Warning: Failed to flush event stream: {e}");
716            }
717        }
718
719        let entry = LogEntry::new(event);
720        self.entries.push(entry);
721        self.current_index = self.entries.len();
722
723        // Check if we should create a snapshot
724        if self.entries.len().is_multiple_of(self.snapshot_interval) {
725            // Snapshot creation will be implemented when we have Buffer
726            // For now, just track that we'd create one here
727        }
728
729        self.current_index - 1
730    }
731
732    /// Get the current event index
733    pub fn current_index(&self) -> usize {
734        self.current_index
735    }
736
737    /// Get the number of events in the log
738    pub fn len(&self) -> usize {
739        self.entries.len()
740    }
741
742    /// Check if the event log is empty
743    pub fn is_empty(&self) -> bool {
744        self.entries.is_empty()
745    }
746
747    /// Can we undo?
748    pub fn can_undo(&self) -> bool {
749        self.current_index > 0
750    }
751
752    /// Can we redo?
753    pub fn can_redo(&self) -> bool {
754        self.current_index < self.entries.len()
755    }
756
757    /// Move back through events (for undo)
758    /// Collects all events up to and including the first write action, returns their inverses
759    /// This processes readonly events (like scrolling) and stops at write events (like Insert/Delete)
760    pub fn undo(&mut self) -> Vec<Event> {
761        let mut inverse_events = Vec::new();
762        let mut found_write_action = false;
763
764        // Keep moving backward until we find a write action
765        while self.can_undo() && !found_write_action {
766            self.current_index -= 1;
767            let event = &self.entries[self.current_index].event;
768
769            // Check if this is a write action - we'll stop after processing it
770            if event.is_write_action() {
771                found_write_action = true;
772            }
773
774            // Try to get the inverse of this event
775            if let Some(inverse) = event.inverse() {
776                inverse_events.push(inverse);
777            }
778            // If no inverse exists (like MoveCursor), we just skip it
779        }
780
781        inverse_events
782    }
783
784    /// Move forward through events (for redo)
785    /// Collects the first write action plus all readonly events after it (until next write action)
786    /// This processes readonly events (like scrolling) with write events (like Insert/Delete)
787    pub fn redo(&mut self) -> Vec<Event> {
788        let mut events = Vec::new();
789        let mut found_write_action = false;
790
791        // Keep moving forward to collect write action and subsequent readonly events
792        while self.can_redo() {
793            let event = self.entries[self.current_index].event.clone();
794
795            // If we've already found a write action and this is another write action, stop
796            if found_write_action && event.is_write_action() {
797                // Don't include this event, it's the next write action
798                break;
799            }
800
801            self.current_index += 1;
802
803            // Mark if we found a write action
804            if event.is_write_action() {
805                found_write_action = true;
806            }
807
808            events.push(event);
809        }
810
811        events
812    }
813
814    /// Get all events from the log
815    pub fn entries(&self) -> &[LogEntry] {
816        &self.entries
817    }
818
819    /// Get events in a range
820    pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
821        &self.entries[range]
822    }
823
824    /// Get the most recent event
825    pub fn last_event(&self) -> Option<&Event> {
826        if self.current_index > 0 {
827            Some(&self.entries[self.current_index - 1].event)
828        } else {
829            None
830        }
831    }
832
833    /// Clear all events (for testing or reset)
834    pub fn clear(&mut self) {
835        self.entries.clear();
836        self.current_index = 0;
837        self.snapshots.clear();
838    }
839
840    /// Save event log to JSON Lines format
841    pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
842        use std::io::Write;
843        let file = std::fs::File::create(path)?;
844        let mut writer = std::io::BufWriter::new(file);
845
846        for entry in &self.entries {
847            let json = serde_json::to_string(entry)?;
848            writeln!(writer, "{json}")?;
849        }
850
851        Ok(())
852    }
853
854    /// Load event log from JSON Lines format
855    pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
856        use std::io::BufRead;
857        let file = std::fs::File::open(path)?;
858        let reader = std::io::BufReader::new(file);
859
860        let mut log = Self::new();
861
862        for line in reader.lines() {
863            let line = line?;
864            if line.trim().is_empty() {
865                continue;
866            }
867            let entry: LogEntry = serde_json::from_str(&line)?;
868            log.entries.push(entry);
869        }
870
871        log.current_index = log.entries.len();
872
873        Ok(log)
874    }
875
876    /// Set snapshot interval
877    pub fn set_snapshot_interval(&mut self, interval: usize) {
878        self.snapshot_interval = interval;
879    }
880}
881
882impl Default for EventLog {
883    fn default() -> Self {
884        Self::new()
885    }
886}
887
888#[cfg(test)]
889mod tests {
890    use super::*;
891
892    // Property-based tests
893    #[cfg(test)]
894    mod property_tests {
895        use super::*;
896        use proptest::prelude::*;
897
898        /// Helper to generate random events
899        fn arb_event() -> impl Strategy<Value = Event> {
900            prop_oneof![
901                // Insert events
902                (0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
903                    position: pos,
904                    text,
905                    cursor_id: CursorId(0),
906                }),
907                // Delete events
908                (0usize..1000, 1usize..50).prop_map(|(pos, len)| Event::Delete {
909                    range: pos..pos + len,
910                    deleted_text: "x".repeat(len),
911                    cursor_id: CursorId(0),
912                }),
913            ]
914        }
915
916        proptest! {
917            /// Event inverse should be truly inverse
918            #[test]
919            fn event_inverse_property(event in arb_event()) {
920                if let Some(inverse) = event.inverse() {
921                    // The inverse of an inverse should be the original
922                    // (for commutative operations)
923                    if let Some(double_inverse) = inverse.inverse() {
924                        match (&event, &double_inverse) {
925                            (Event::Insert { position: p1, text: t1, .. },
926                             Event::Insert { position: p2, text: t2, .. }) => {
927                                assert_eq!(p1, p2);
928                                assert_eq!(t1, t2);
929                            }
930                            (Event::Delete { range: r1, deleted_text: dt1, .. },
931                             Event::Delete { range: r2, deleted_text: dt2, .. }) => {
932                                assert_eq!(r1, r2);
933                                assert_eq!(dt1, dt2);
934                            }
935                            _ => {}
936                        }
937                    }
938                }
939            }
940
941            /// Undo then redo should restore state
942            #[test]
943            fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
944                let mut log = EventLog::new();
945
946                // Append all events
947                for event in &events {
948                    log.append(event.clone());
949                }
950
951                let after_append = log.current_index();
952
953                // Undo all
954                let mut undo_count = 0;
955                while log.can_undo() {
956                    log.undo();
957                    undo_count += 1;
958                }
959
960                assert_eq!(log.current_index(), 0);
961                assert_eq!(undo_count, events.len());
962
963                // Redo all
964                let mut redo_count = 0;
965                while log.can_redo() {
966                    log.redo();
967                    redo_count += 1;
968                }
969
970                assert_eq!(log.current_index(), after_append);
971                assert_eq!(redo_count, events.len());
972            }
973
974            /// Appending after undo should truncate redo history
975            #[test]
976            fn append_after_undo_truncates(
977                initial_events in prop::collection::vec(arb_event(), 2..10),
978                new_event in arb_event()
979            ) {
980                let mut log = EventLog::new();
981
982                for event in &initial_events {
983                    log.append(event.clone());
984                }
985
986                // Undo at least one
987                log.undo();
988                let index_after_undo = log.current_index();
989
990                // Append new event
991                log.append(new_event);
992
993                // Should not be able to redo past the new event
994                assert_eq!(log.current_index(), index_after_undo + 1);
995                assert!(!log.can_redo());
996            }
997        }
998    }
999
1000    #[test]
1001    fn test_event_log_append() {
1002        let mut log = EventLog::new();
1003        let event = Event::Insert {
1004            position: 0,
1005            text: "hello".to_string(),
1006            cursor_id: CursorId(0),
1007        };
1008
1009        let index = log.append(event);
1010        assert_eq!(index, 0);
1011        assert_eq!(log.current_index(), 1);
1012        assert_eq!(log.entries().len(), 1);
1013    }
1014
1015    #[test]
1016    fn test_undo_redo() {
1017        let mut log = EventLog::new();
1018
1019        log.append(Event::Insert {
1020            position: 0,
1021            text: "a".to_string(),
1022            cursor_id: CursorId(0),
1023        });
1024
1025        log.append(Event::Insert {
1026            position: 1,
1027            text: "b".to_string(),
1028            cursor_id: CursorId(0),
1029        });
1030
1031        assert_eq!(log.current_index(), 2);
1032        assert!(log.can_undo());
1033        assert!(!log.can_redo());
1034
1035        log.undo();
1036        assert_eq!(log.current_index(), 1);
1037        assert!(log.can_undo());
1038        assert!(log.can_redo());
1039
1040        log.undo();
1041        assert_eq!(log.current_index(), 0);
1042        assert!(!log.can_undo());
1043        assert!(log.can_redo());
1044
1045        log.redo();
1046        assert_eq!(log.current_index(), 1);
1047    }
1048
1049    #[test]
1050    fn test_event_inverse() {
1051        let insert = Event::Insert {
1052            position: 5,
1053            text: "hello".to_string(),
1054            cursor_id: CursorId(0),
1055        };
1056
1057        let inverse = insert.inverse().unwrap();
1058        match inverse {
1059            Event::Delete {
1060                range,
1061                deleted_text,
1062                ..
1063            } => {
1064                assert_eq!(range, 5..10);
1065                assert_eq!(deleted_text, "hello");
1066            }
1067            _ => panic!("Expected Delete event"),
1068        }
1069    }
1070
1071    #[test]
1072    fn test_truncate_on_new_event_after_undo() {
1073        let mut log = EventLog::new();
1074
1075        log.append(Event::Insert {
1076            position: 0,
1077            text: "a".to_string(),
1078            cursor_id: CursorId(0),
1079        });
1080
1081        log.append(Event::Insert {
1082            position: 1,
1083            text: "b".to_string(),
1084            cursor_id: CursorId(0),
1085        });
1086
1087        log.undo();
1088        assert_eq!(log.entries().len(), 2);
1089
1090        // Adding new event should truncate the future
1091        log.append(Event::Insert {
1092            position: 1,
1093            text: "c".to_string(),
1094            cursor_id: CursorId(0),
1095        });
1096
1097        assert_eq!(log.entries().len(), 2);
1098        assert_eq!(log.current_index(), 2);
1099    }
1100
1101    /// Test for v0.1.77 panic: "range end index 148 out of range for slice of length 125"
1102    ///
1103    /// The bug occurs when:
1104    /// 1. Make many changes (entries grows)
1105    /// 2. mark_saved() sets saved_at_index to current position
1106    /// 3. Undo several times
1107    /// 4. Make new changes - this truncates entries but saved_at_index stays
1108    /// 5. is_at_saved_position() panics on out-of-bounds slice access
1109    #[test]
1110    fn test_is_at_saved_position_after_truncate() {
1111        let mut log = EventLog::new();
1112
1113        // Step 1: Make many changes
1114        for i in 0..150 {
1115            log.append(Event::Insert {
1116                position: i,
1117                text: "x".to_string(),
1118                cursor_id: CursorId(0),
1119            });
1120        }
1121
1122        assert_eq!(log.entries().len(), 150);
1123        assert_eq!(log.current_index(), 150);
1124
1125        // Step 2: Save - this sets saved_at_index = 150
1126        log.mark_saved();
1127
1128        // Step 3: Undo 30 times - current_index goes to 120, but entries stay at 150
1129        for _ in 0..30 {
1130            log.undo();
1131        }
1132        assert_eq!(log.current_index(), 120);
1133        assert_eq!(log.entries().len(), 150);
1134
1135        // Step 4: Make new changes - this truncates entries to 120, then adds new
1136        log.append(Event::Insert {
1137            position: 0,
1138            text: "NEW".to_string(),
1139            cursor_id: CursorId(0),
1140        });
1141
1142        // Now entries.len() = 121, but saved_at_index = 150
1143        assert_eq!(log.entries().len(), 121);
1144        assert_eq!(log.current_index(), 121);
1145
1146        // Step 5: Call is_at_saved_position() - THIS PANICS in v0.1.77
1147        // The code does: self.entries[start..end] where end = saved_at_index = 150
1148        // but entries.len() = 121, so 150 is out of bounds
1149        let result = log.is_at_saved_position();
1150
1151        // After fix: should return false (we're not at saved position, we branched off)
1152        assert!(
1153            !result,
1154            "Should not be at saved position after undo + new edit"
1155        );
1156    }
1157}