Skip to main content

fresh/model/
event.rs

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/// 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        /// Optional URL for OSC 8 terminal hyperlinks
91        url: Option<String>,
92    },
93
94    /// Remove overlay by handle
95    RemoveOverlay {
96        handle: OverlayHandle,
97    },
98
99    /// Remove all overlays in a range
100    RemoveOverlaysInRange {
101        range: Range<usize>,
102    },
103
104    /// Clear all overlays in a namespace
105    ClearNamespace {
106        namespace: OverlayNamespace,
107    },
108
109    /// Clear all overlays
110    ClearOverlays,
111
112    /// Show a popup
113    ShowPopup {
114        popup: PopupData,
115    },
116
117    /// Hide the topmost popup
118    HidePopup,
119
120    /// Clear all popups
121    ClearPopups,
122
123    /// Navigate popup selection (for list popups)
124    PopupSelectNext,
125    PopupSelectPrev,
126    PopupPageDown,
127    PopupPageUp,
128
129    /// Margin events
130    /// Add a margin annotation
131    AddMarginAnnotation {
132        line: usize,
133        position: MarginPositionData,
134        content: MarginContentData,
135        annotation_id: Option<String>,
136    },
137
138    /// Remove margin annotation by ID
139    RemoveMarginAnnotation {
140        annotation_id: String,
141    },
142
143    /// Remove all margin annotations at a specific line
144    RemoveMarginAnnotationsAtLine {
145        line: usize,
146        position: MarginPositionData,
147    },
148
149    /// Clear all margin annotations in a position
150    ClearMarginPosition {
151        position: MarginPositionData,
152    },
153
154    /// Clear all margin annotations
155    ClearMargins,
156
157    /// Enable/disable line numbers
158    SetLineNumbers {
159        enabled: bool,
160    },
161
162    /// Split view events
163    /// Split the active pane
164    SplitPane {
165        direction: SplitDirection,
166        new_buffer_id: BufferId,
167        ratio: f32,
168    },
169
170    /// Close a split pane
171    CloseSplit {
172        split_id: SplitId,
173    },
174
175    /// Set the active split pane
176    SetActiveSplit {
177        split_id: SplitId,
178    },
179
180    /// Adjust the split ratio
181    AdjustSplitRatio {
182        split_id: SplitId,
183        delta: f32,
184    },
185
186    /// Navigate to next split
187    NextSplit,
188
189    /// Navigate to previous split
190    PrevSplit,
191
192    /// Batch of events that should be undone/redone atomically
193    /// Used for multi-cursor operations where all cursors perform the same action
194    Batch {
195        events: Vec<Event>,
196        description: String,
197    },
198
199    /// Efficient bulk edit that stores tree snapshots for O(1) undo/redo
200    /// Used for multi-cursor operations, toggle comment, indent/dedent, etc.
201    /// This avoids O(n²) complexity by applying all edits in a single tree pass.
202    ///
203    /// Key insight: PieceTree uses Arc<PieceTreeNode> (persistent data structure),
204    /// so storing trees for undo/redo is O(1) (Arc clone), not O(n) (content copy).
205    BulkEdit {
206        /// Buffer state before the edit (for undo)
207        #[serde(skip)]
208        old_snapshot: Option<Arc<BufferSnapshot>>,
209        /// Buffer state after the edit (for redo)
210        #[serde(skip)]
211        new_snapshot: Option<Arc<BufferSnapshot>>,
212        /// Cursor states before the edit
213        old_cursors: Vec<(CursorId, usize, Option<usize>)>,
214        /// Cursor states after the edit
215        new_cursors: Vec<(CursorId, usize, Option<usize>)>,
216        /// Human-readable description
217        description: String,
218    },
219}
220
221/// Overlay face data for events (must be serializable)
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub enum OverlayFace {
224    Underline {
225        color: (u8, u8, u8), // RGB color
226        style: UnderlineStyle,
227    },
228    Background {
229        color: (u8, u8, u8),
230    },
231    Foreground {
232        color: (u8, u8, u8),
233    },
234    /// Full style with theme-aware colors
235    ///
236    /// Uses OverlayOptions which supports both RGB colors and theme keys.
237    /// Theme keys are resolved at render time.
238    Style {
239        options: OverlayOptions,
240    },
241}
242
243impl OverlayFace {
244    /// Create an OverlayFace from OverlayOptions
245    pub fn from_options(options: OverlayOptions) -> Self {
246        OverlayFace::Style { options }
247    }
248}
249
250/// Underline style for overlays
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub enum UnderlineStyle {
253    Straight,
254    Wavy,
255    Dotted,
256    Dashed,
257}
258
259/// What kind of popup this is — determines input handling behavior.
260///
261/// This replaces the old approach of inferring popup kind from the title string,
262/// which broke when titles were translated to non-English locales.
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
264pub enum PopupKindHint {
265    /// LSP completion popup - supports type-to-filter, Tab/Enter accept
266    Completion,
267    /// Generic list popup - navigate and select
268    #[default]
269    List,
270    /// Generic text popup - read-only
271    Text,
272}
273
274/// Popup data for events (must be serializable)
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct PopupData {
277    /// Popup kind — determines input handling behavior.
278    #[serde(default)]
279    pub kind: PopupKindHint,
280    pub title: Option<String>,
281    /// Optional description text shown above the content
282    #[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/// Popup content for events
294#[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/// Popup list item for events
304#[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/// Popup position for events
313#[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/// Margin position for events
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
325pub enum MarginPositionData {
326    Left,
327    Right,
328}
329
330/// Margin content for events
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub enum MarginContentData {
333    Text(String),
334    Symbol {
335        text: String,
336        color: Option<(u8, u8, u8)>, // RGB color
337    },
338    Empty,
339}
340
341impl Event {
342    /// Returns the inverse event for undo functionality
343    /// Uses UNDO_SENTINEL cursor_id to avoid moving the cursor during undo
344    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                // Invert all events in the batch in reverse order
368                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                // To undo adding a cursor, we remove it (store its state for redo)
382                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                // To undo removing a cursor, we add it back
394                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                // Invert by swapping old and new positions
410                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                // Overlays are ephemeral decorations, not undoable
422                None
423            }
424            Self::RemoveOverlay { .. } => {
425                // Overlays are ephemeral decorations, not undoable
426                None
427            }
428            Self::ClearNamespace { .. } => {
429                // Overlays are ephemeral decorations, not undoable
430                None
431            }
432            Self::Scroll { line_offset } => Some(Self::Scroll {
433                line_offset: -line_offset,
434            }),
435            Self::SetViewport { top_line: _ } => {
436                // Can't invert without knowing old top_line
437                None
438            }
439            Self::ChangeMode { mode: _ } => {
440                // Can't invert without knowing old mode
441                None
442            }
443            Self::BulkEdit {
444                old_snapshot,
445                new_snapshot,
446                old_cursors,
447                new_cursors,
448                description,
449            } => {
450                // Inverse swaps both snapshots and cursor states
451                // For undo: old becomes new, new becomes old
452                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            // Other events (popups, margins, splits, etc.) are not automatically invertible
461            _ => None,
462        }
463    }
464
465    /// Returns true if this event modifies the buffer content
466    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    /// Returns true if this event is a write action (modifies state in a way that should be undoable)
475    /// Returns false for readonly actions like cursor movement, scrolling, viewport changes, etc.
476    ///
477    /// Write actions include:
478    /// - Buffer modifications (Insert, Delete)
479    /// - Cursor structure changes (AddCursor, RemoveCursor)
480    /// - Batches containing write actions
481    ///
482    /// Readonly actions include:
483    /// - Cursor movement (MoveCursor)
484    /// - Scrolling and viewport changes (Scroll, SetViewport)
485    /// - UI events (overlays, popups, margins, mode changes, etc.)
486    pub fn is_write_action(&self) -> bool {
487        match self {
488            // Buffer modifications are write actions
489            Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
490
491            // Adding/removing cursors are write actions (structural changes)
492            Self::AddCursor { .. } | Self::RemoveCursor { .. } => true,
493
494            // Batches are write actions if they contain any write actions
495            Self::Batch { events, .. } => events.iter().any(|e| e.is_write_action()),
496
497            // All other events are readonly (movement, scrolling, UI, etc.)
498            _ => false,
499        }
500    }
501
502    /// Returns the cursor ID associated with this event, if any
503    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/// A log entry containing an event and metadata
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct LogEntry {
518    /// The event
519    pub event: Event,
520
521    /// Timestamp when the event occurred (milliseconds since epoch)
522    pub timestamp: u64,
523
524    /// Optional description for debugging
525    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/// Snapshot of editor state for fast undo/redo
547#[derive(Debug, Clone)]
548pub struct Snapshot {
549    /// Index in the event log where this snapshot was taken
550    pub log_index: usize,
551
552    /// Buffer content at this point (stored as ChunkTree reference)
553    /// For now we'll use a placeholder - will be filled in when we implement Buffer
554    pub buffer_state: (),
555
556    /// Cursor positions at this point
557    pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
558}
559
560/// The event log - append-only log of all events
561pub struct EventLog {
562    /// All logged events
563    entries: Vec<LogEntry>,
564
565    /// Current position in the log (for undo/redo)
566    current_index: usize,
567
568    /// Periodic snapshots for fast seeking
569    snapshots: Vec<Snapshot>,
570
571    /// How often to create snapshots (every N events)
572    snapshot_interval: usize,
573
574    /// Optional file for streaming events to disk (runtime only)
575    #[cfg(feature = "runtime")]
576    stream_file: Option<std::fs::File>,
577
578    /// Index at which the buffer was last saved (for tracking modified status)
579    /// When current_index equals saved_at_index, the buffer is not modified
580    saved_at_index: Option<usize>,
581}
582
583impl EventLog {
584    /// Create a new empty event log
585    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), // New buffer starts at "saved" state (index 0)
594        }
595    }
596
597    /// Mark the current position as the saved point
598    /// Call this when the buffer is saved to disk
599    pub fn mark_saved(&mut self) {
600        self.saved_at_index = Some(self.current_index);
601    }
602
603    /// Check if the buffer is at the saved position (not modified)
604    /// Returns true if we're at the saved position OR if all events between
605    /// saved_at_index and current_index are readonly (don't modify buffer content)
606    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                // Check if all events between saved position and current position
612                // are readonly (don't modify buffer content)
613                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                // All events in range [start, end) must be readonly
620                self.entries[start..end]
621                    .iter()
622                    .all(|entry| !entry.event.modifies_buffer())
623            }
624        }
625    }
626
627    /// Enable streaming events to a file (runtime only)
628    #[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        // Write header
639        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    /// Disable streaming (runtime only)
649    #[cfg(feature = "runtime")]
650    pub fn disable_streaming(&mut self) {
651        self.stream_file = None;
652    }
653
654    /// Log rendering state (for debugging, runtime only)
655    #[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    /// Log keystroke (for debugging, runtime only)
684    #[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    /// Append an event to the log
706    pub fn append(&mut self, event: Event) -> usize {
707        // When redo history exists (after undo), only write actions are logged.
708        // Non-write events (MoveCursor, Scroll, etc.) are still applied to the
709        // editor state but not recorded in the log, preserving redo history.
710        // This matches standard editor behavior (VS Code, Sublime, etc.) where
711        // navigation after undo does not destroy the redo chain.
712        if self.current_index < self.entries.len() {
713            if event.is_write_action() {
714                // Write action: truncate redo history and log normally
715                self.entries.truncate(self.current_index);
716
717                // Invalidate saved_at_index if it pointed to a truncated entry
718                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                // Non-write event while redo exists: skip logging to preserve redo
725                return self.current_index;
726            }
727        }
728
729        // Stream event to file if enabled (runtime only)
730        #[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            // Write JSON line and flush immediately for real-time logging
741            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        // Check if we should create a snapshot
754        if self.entries.len().is_multiple_of(self.snapshot_interval) {
755            // Snapshot creation will be implemented when we have Buffer
756            // For now, just track that we'd create one here
757        }
758
759        self.current_index - 1
760    }
761
762    /// Get the current event index
763    pub fn current_index(&self) -> usize {
764        self.current_index
765    }
766
767    /// Get the number of events in the log
768    pub fn len(&self) -> usize {
769        self.entries.len()
770    }
771
772    /// Check if the event log is empty
773    pub fn is_empty(&self) -> bool {
774        self.entries.is_empty()
775    }
776
777    /// Can we undo?
778    pub fn can_undo(&self) -> bool {
779        self.current_index > 0
780    }
781
782    /// Can we redo?
783    pub fn can_redo(&self) -> bool {
784        self.current_index < self.entries.len()
785    }
786
787    /// Move back through events (for undo)
788    /// Collects all events up to and including the first write action, returns their inverses
789    /// This processes readonly events (like scrolling) and stops at write events (like Insert/Delete)
790    pub fn undo(&mut self) -> Vec<Event> {
791        let mut inverse_events = Vec::new();
792        let mut found_write_action = false;
793
794        // Keep moving backward until we find a write action
795        while self.can_undo() && !found_write_action {
796            self.current_index -= 1;
797            let event = &self.entries[self.current_index].event;
798
799            // Check if this is a write action - we'll stop after processing it
800            if event.is_write_action() {
801                found_write_action = true;
802            }
803
804            // Try to get the inverse of this event
805            if let Some(inverse) = event.inverse() {
806                inverse_events.push(inverse);
807            }
808            // If no inverse exists (like MoveCursor), we just skip it
809        }
810
811        inverse_events
812    }
813
814    /// Move forward through events (for redo)
815    /// Collects the first write action plus all readonly events after it (until next write action)
816    /// This processes readonly events (like scrolling) with write events (like Insert/Delete)
817    pub fn redo(&mut self) -> Vec<Event> {
818        let mut events = Vec::new();
819        let mut found_write_action = false;
820
821        // Keep moving forward to collect write action and subsequent readonly events
822        while self.can_redo() {
823            let event = self.entries[self.current_index].event.clone();
824
825            // If we've already found a write action and this is another write action, stop
826            if found_write_action && event.is_write_action() {
827                // Don't include this event, it's the next write action
828                break;
829            }
830
831            self.current_index += 1;
832
833            // Mark if we found a write action
834            if event.is_write_action() {
835                found_write_action = true;
836            }
837
838            events.push(event);
839        }
840
841        events
842    }
843
844    /// Get all events from the log
845    pub fn entries(&self) -> &[LogEntry] {
846        &self.entries
847    }
848
849    /// Get events in a range
850    pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
851        &self.entries[range]
852    }
853
854    /// Get the most recent event
855    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    /// Clear all events (for testing or reset)
864    pub fn clear(&mut self) {
865        self.entries.clear();
866        self.current_index = 0;
867        self.snapshots.clear();
868    }
869
870    /// Save event log to JSON Lines format
871    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    /// Load event log from JSON Lines format
885    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    /// Set snapshot interval
907    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    // Property-based tests
923    #[cfg(test)]
924    mod property_tests {
925        use super::*;
926        use proptest::prelude::*;
927
928        /// Helper to generate random events
929        fn arb_event() -> impl Strategy<Value = Event> {
930            prop_oneof![
931                // Insert events
932                (0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
933                    position: pos,
934                    text,
935                    cursor_id: CursorId(0),
936                }),
937                // Delete events
938                (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            /// Event inverse should be truly inverse
948            #[test]
949            fn event_inverse_property(event in arb_event()) {
950                if let Some(inverse) = event.inverse() {
951                    // The inverse of an inverse should be the original
952                    // (for commutative operations)
953                    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            /// Undo then redo should restore state
972            #[test]
973            fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
974                let mut log = EventLog::new();
975
976                // Append all events
977                for event in &events {
978                    log.append(event.clone());
979                }
980
981                let after_append = log.current_index();
982
983                // Undo all
984                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                // Redo all
994                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            /// Appending after undo should truncate redo history
1005            #[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                // Undo at least one
1017                log.undo();
1018                let index_after_undo = log.current_index();
1019
1020                // Append new event
1021                log.append(new_event);
1022
1023                // Should not be able to redo past the new event
1024                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        // Adding new event should truncate the future
1121        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        // Regression test: navigation after undo should not destroy redo history.
1134        // Standard editors (VS Code, Sublime) allow: type, undo, move around, redo.
1135        let mut log = EventLog::new();
1136
1137        // Type 'a' (Insert + MoveCursor)
1138        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        // Undo (walks back past MoveCursor and Insert)
1155        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        // Navigate (MoveCursor) — should NOT destroy redo
1161        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        // Redo should still work
1176        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        // Write actions after undo SHOULD still clear redo history
1186        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        // New write action should truncate redo
1198        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 for v0.1.77 panic: "range end index 148 out of range for slice of length 125"
1210    ///
1211    /// The bug occurs when:
1212    /// 1. Make many changes (entries grows)
1213    /// 2. mark_saved() sets saved_at_index to current position
1214    /// 3. Undo several times
1215    /// 4. Make new changes - this truncates entries but saved_at_index stays
1216    /// 5. is_at_saved_position() panics on out-of-bounds slice access
1217    #[test]
1218    fn test_is_at_saved_position_after_truncate() {
1219        let mut log = EventLog::new();
1220
1221        // Step 1: Make many changes
1222        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        // Step 2: Save - this sets saved_at_index = 150
1234        log.mark_saved();
1235
1236        // Step 3: Undo 30 times - current_index goes to 120, but entries stay at 150
1237        for _ in 0..30 {
1238            log.undo();
1239        }
1240        assert_eq!(log.current_index(), 120);
1241        assert_eq!(log.entries().len(), 150);
1242
1243        // Step 4: Make new changes - this truncates entries to 120, then adds new
1244        log.append(Event::Insert {
1245            position: 0,
1246            text: "NEW".to_string(),
1247            cursor_id: CursorId(0),
1248        });
1249
1250        // Now entries.len() = 121, but saved_at_index = 150
1251        assert_eq!(log.entries().len(), 121);
1252        assert_eq!(log.current_index(), 121);
1253
1254        // Step 5: Call is_at_saved_position() - THIS PANICS in v0.1.77
1255        // The code does: self.entries[start..end] where end = saved_at_index = 150
1256        // but entries.len() = 121, so 150 is out of bounds
1257        let result = log.is_at_saved_position();
1258
1259        // After fix: should return false (we're not at saved position, we branched off)
1260        assert!(
1261            !result,
1262            "Should not be at saved position after undo + new edit"
1263        );
1264    }
1265}