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        /// Edit operations as (position, delete_len, insert_len), sorted descending by position.
219        /// Used to replay marker adjustments on undo/redo:
220        /// - On redo: replayed as-is (same adjustments as the forward path)
221        /// - On undo: inverse() swaps del_len/ins_len (reverse adjustments)
222        #[serde(default)]
223        edits: Vec<(usize, usize, usize)>,
224        /// Marker positions displaced by deletions: (marker_id_raw, original_byte_position).
225        /// On undo, after marker adjustments, these markers are restored to their
226        /// original positions. This fixes the limitation where markers inside a
227        /// deleted range collapse and can't be precisely restored by undo.
228        #[serde(default)]
229        displaced_markers: Vec<(u64, usize)>,
230    },
231}
232
233/// Overlay face data for events (must be serializable)
234#[derive(Debug, Clone, Serialize, Deserialize)]
235pub enum OverlayFace {
236    Underline {
237        color: (u8, u8, u8), // RGB color
238        style: UnderlineStyle,
239    },
240    Background {
241        color: (u8, u8, u8),
242    },
243    Foreground {
244        color: (u8, u8, u8),
245    },
246    /// Full style with theme-aware colors
247    ///
248    /// Uses OverlayOptions which supports both RGB colors and theme keys.
249    /// Theme keys are resolved at render time.
250    Style {
251        options: OverlayOptions,
252    },
253}
254
255impl OverlayFace {
256    /// Create an OverlayFace from OverlayOptions
257    pub fn from_options(options: OverlayOptions) -> Self {
258        OverlayFace::Style { options }
259    }
260}
261
262/// Underline style for overlays
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
264pub enum UnderlineStyle {
265    Straight,
266    Wavy,
267    Dotted,
268    Dashed,
269}
270
271/// What kind of popup this is — determines input handling behavior.
272///
273/// This replaces the old approach of inferring popup kind from the title string,
274/// which broke when titles were translated to non-English locales.
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
276pub enum PopupKindHint {
277    /// LSP completion popup - supports type-to-filter, Tab/Enter accept
278    Completion,
279    /// Generic list popup - navigate and select
280    #[default]
281    List,
282    /// Generic text popup - read-only
283    Text,
284}
285
286/// Popup data for events (must be serializable)
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct PopupData {
289    /// Popup kind — determines input handling behavior.
290    #[serde(default)]
291    pub kind: PopupKindHint,
292    pub title: Option<String>,
293    /// Optional description text shown above the content
294    #[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/// Popup content for events
306#[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/// Popup list item for events
316#[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/// Popup position for events
325#[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    /// Anchored above the status bar at a specific column. Used for the
337    /// LSP-status popup so it appears directly above the LSP segment of
338    /// the status bar that opened it.
339    AboveStatusBarAt {
340        x: u16,
341    },
342}
343
344/// Margin position for events
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
346pub enum MarginPositionData {
347    Left,
348    Right,
349}
350
351/// Margin content for events
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub enum MarginContentData {
354    Text(String),
355    Symbol {
356        text: String,
357        color: Option<(u8, u8, u8)>, // RGB color
358    },
359    Empty,
360}
361
362impl Event {
363    /// Returns the inverse event for undo functionality
364    /// Uses UNDO_SENTINEL cursor_id to avoid moving the cursor during undo
365    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                // Invert all events in the batch in reverse order
389                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                // To undo adding a cursor, we remove it (store its state for redo)
403                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                // To undo removing a cursor, we add it back
415                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                // Invert by swapping old and new positions
431                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                // Overlays are ephemeral decorations, not undoable
443                None
444            }
445            Self::RemoveOverlay { .. } => {
446                // Overlays are ephemeral decorations, not undoable
447                None
448            }
449            Self::ClearNamespace { .. } => {
450                // Overlays are ephemeral decorations, not undoable
451                None
452            }
453            Self::Scroll { line_offset } => Some(Self::Scroll {
454                line_offset: -line_offset,
455            }),
456            Self::SetViewport { top_line: _ } => {
457                // Can't invert without knowing old top_line
458                None
459            }
460            Self::ChangeMode { mode: _ } => {
461                // Can't invert without knowing old mode
462                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                // Inverse swaps both snapshots, cursor states, and edit directions.
474                // Swapping del_len/ins_len makes undo apply reverse marker adjustments.
475                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 only applies to undo (restoring original positions).
488                    // The redo direction doesn't need them — forward adjustments are correct.
489                    // We pass them through so undo can use them.
490                    displaced_markers: displaced_markers.clone(),
491                })
492            }
493            // Other events (popups, margins, splits, etc.) are not automatically invertible
494            _ => None,
495        }
496    }
497
498    /// Returns true if this event modifies the buffer content
499    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    /// Returns true if this event is a write action (modifies state in a way that should be undoable)
508    /// Returns false for readonly actions like cursor movement, scrolling, viewport changes, etc.
509    ///
510    /// Write actions include:
511    /// - Buffer modifications (Insert, Delete)
512    /// - Cursor structure changes (AddCursor, RemoveCursor)
513    /// - Batches containing write actions
514    ///
515    /// Readonly actions include:
516    /// - Cursor movement (MoveCursor)
517    /// - Scrolling and viewport changes (Scroll, SetViewport)
518    /// - UI events (overlays, popups, margins, mode changes, etc.)
519    pub fn is_write_action(&self) -> bool {
520        match self {
521            // Buffer modifications are write actions
522            Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
523
524            // Adding/removing cursors are write actions (structural changes)
525            Self::AddCursor { .. } | Self::RemoveCursor { .. } => true,
526
527            // Batches are write actions if they contain any write actions
528            Self::Batch { events, .. } => events.iter().any(|e| e.is_write_action()),
529
530            // All other events are readonly (movement, scrolling, UI, etc.)
531            _ => false,
532        }
533    }
534
535    /// Returns the cursor ID associated with this event, if any
536    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/// A log entry containing an event and metadata
549#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct LogEntry {
551    /// The event
552    pub event: Event,
553
554    /// Timestamp when the event occurred (milliseconds since epoch)
555    pub timestamp: u64,
556
557    /// Optional description for debugging
558    pub description: Option<String>,
559
560    /// Markers displaced by deletions in this event.
561    /// Stored as (marker_id_raw, original_byte_position).
562    /// When this event is undone, the inverse Insert restores these markers
563    /// to their exact original positions.
564    #[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/// Snapshot of editor state for fast undo/redo
588#[derive(Debug, Clone)]
589pub struct Snapshot {
590    /// Index in the event log where this snapshot was taken
591    pub log_index: usize,
592
593    /// Buffer content at this point (stored as ChunkTree reference)
594    /// For now we'll use a placeholder - will be filled in when we implement Buffer
595    pub buffer_state: (),
596
597    /// Cursor positions at this point
598    pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
599}
600
601/// The event log - append-only log of all events
602pub struct EventLog {
603    /// All logged events
604    entries: Vec<LogEntry>,
605
606    /// Current position in the log (for undo/redo)
607    current_index: usize,
608
609    /// Periodic snapshots for fast seeking
610    snapshots: Vec<Snapshot>,
611
612    /// How often to create snapshots (every N events)
613    snapshot_interval: usize,
614
615    /// Optional file for streaming events to disk (runtime only)
616    #[cfg(feature = "runtime")]
617    stream_file: Option<std::fs::File>,
618
619    /// Index at which the buffer was last saved (for tracking modified status)
620    /// When current_index equals saved_at_index, the buffer is not modified
621    saved_at_index: Option<usize>,
622}
623
624impl EventLog {
625    /// Create a new empty event log
626    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), // New buffer starts at "saved" state (index 0)
635        }
636    }
637
638    /// Mark the current position as the saved point
639    /// Call this when the buffer is saved to disk
640    pub fn mark_saved(&mut self) {
641        self.saved_at_index = Some(self.current_index);
642    }
643
644    /// Invalidate the saved position so the buffer is always considered modified.
645    /// Call this after hot exit recovery, where the buffer content differs from
646    /// disk but the event log has no record of the changes.
647    pub fn clear_saved_position(&mut self) {
648        self.saved_at_index = None;
649    }
650
651    /// Check if the buffer is at the saved position (not modified)
652    /// Returns true if we're at the saved position OR if all events between
653    /// saved_at_index and current_index are readonly (don't modify buffer content)
654    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                // Check if all events between saved position and current position
660                // are readonly (don't modify buffer content)
661                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                // All events in range [start, end) must be readonly
668                self.entries[start..end]
669                    .iter()
670                    .all(|entry| !entry.event.modifies_buffer())
671            }
672        }
673    }
674
675    /// Enable streaming events to a file (runtime only)
676    #[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        // Write header
687        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    /// Disable streaming (runtime only)
697    #[cfg(feature = "runtime")]
698    pub fn disable_streaming(&mut self) {
699        self.stream_file = None;
700    }
701
702    /// Log rendering state (for debugging, runtime only)
703    #[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    /// Log keystroke (for debugging, runtime only)
732    #[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    /// Append an event to the log
754    pub fn append(&mut self, event: Event) -> usize {
755        // When redo history exists (after undo), only write actions are logged.
756        // Non-write events (MoveCursor, Scroll, etc.) are still applied to the
757        // editor state but not recorded in the log, preserving redo history.
758        // This matches standard editor behavior (VS Code, Sublime, etc.) where
759        // navigation after undo does not destroy the redo chain.
760        if self.current_index < self.entries.len() {
761            if event.is_write_action() {
762                // Write action: truncate redo history and log normally
763                self.entries.truncate(self.current_index);
764
765                // Invalidate saved_at_index if it pointed to a truncated entry
766                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                // Non-write event while redo exists: skip logging to preserve redo
773                return self.current_index;
774            }
775        }
776
777        // Stream event to file if enabled (runtime only)
778        #[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            // Write JSON line and flush immediately for real-time logging
789            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        // Check if we should create a snapshot
802        if self.entries.len().is_multiple_of(self.snapshot_interval) {
803            // Snapshot creation will be implemented when we have Buffer
804            // For now, just track that we'd create one here
805        }
806
807        self.current_index - 1
808    }
809
810    /// Set displaced markers on the last appended entry.
811    /// Call this right after `append()` to record markers that were inside
812    /// the deleted range, so undo can restore them to exact positions.
813    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    /// Get the current event index
820    pub fn current_index(&self) -> usize {
821        self.current_index
822    }
823
824    /// Get the number of events in the log
825    pub fn len(&self) -> usize {
826        self.entries.len()
827    }
828
829    /// Check if the event log is empty
830    pub fn is_empty(&self) -> bool {
831        self.entries.is_empty()
832    }
833
834    /// Can we undo?
835    pub fn can_undo(&self) -> bool {
836        self.current_index > 0
837    }
838
839    /// Can we redo?
840    pub fn can_redo(&self) -> bool {
841        self.current_index < self.entries.len()
842    }
843
844    /// Move back through events (for undo)
845    /// Collects all events up to and including the first write action, returns their inverses.
846    /// Each inverse event is paired with displaced markers from the original event,
847    /// which should be restored after applying the inverse Insert.
848    /// This processes readonly events (like scrolling) and stops at write events (like Insert/Delete)
849    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        // Keep moving backward until we find a write action
854        while self.can_undo() && !found_write_action {
855            self.current_index -= 1;
856            let entry = &self.entries[self.current_index];
857
858            // Check if this is a write action - we'll stop after processing it
859            if entry.event.is_write_action() {
860                found_write_action = true;
861            }
862
863            // Try to get the inverse of this event
864            if let Some(inverse) = entry.event.inverse() {
865                inverse_events.push((inverse, entry.displaced_markers.clone()));
866            }
867            // If no inverse exists (like MoveCursor), we just skip it
868        }
869
870        inverse_events
871    }
872
873    /// Move forward through events (for redo)
874    /// Collects the first write action plus all readonly events after it (until next write action)
875    /// This processes readonly events (like scrolling) with write events (like Insert/Delete)
876    pub fn redo(&mut self) -> Vec<Event> {
877        let mut events = Vec::new();
878        let mut found_write_action = false;
879
880        // Keep moving forward to collect write action and subsequent readonly events
881        while self.can_redo() {
882            let event = self.entries[self.current_index].event.clone();
883
884            // If we've already found a write action and this is another write action, stop
885            if found_write_action && event.is_write_action() {
886                // Don't include this event, it's the next write action
887                break;
888            }
889
890            self.current_index += 1;
891
892            // Mark if we found a write action
893            if event.is_write_action() {
894                found_write_action = true;
895            }
896
897            events.push(event);
898        }
899
900        events
901    }
902
903    /// Get all events from the log
904    pub fn entries(&self) -> &[LogEntry] {
905        &self.entries
906    }
907
908    /// Get events in a range
909    pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
910        &self.entries[range]
911    }
912
913    /// Get the most recent event
914    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    /// Clear all events (for testing or reset)
923    pub fn clear(&mut self) {
924        self.entries.clear();
925        self.current_index = 0;
926        self.snapshots.clear();
927    }
928
929    /// Save event log to JSON Lines format
930    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    /// Load event log from JSON Lines format
944    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    /// Set snapshot interval
966    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    // Property-based tests
982    #[cfg(test)]
983    mod property_tests {
984        use super::*;
985        use proptest::prelude::*;
986
987        /// Helper to generate random events
988        fn arb_event() -> impl Strategy<Value = Event> {
989            prop_oneof![
990                // Insert events
991                (0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
992                    position: pos,
993                    text,
994                    cursor_id: CursorId(0),
995                }),
996                // Delete events
997                (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            /// Event inverse should be truly inverse
1007            #[test]
1008            fn event_inverse_property(event in arb_event()) {
1009                if let Some(inverse) = event.inverse() {
1010                    // The inverse of an inverse should be the original
1011                    // (for commutative operations)
1012                    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            /// Undo then redo should restore state
1031            #[test]
1032            fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
1033                let mut log = EventLog::new();
1034
1035                // Append all events
1036                for event in &events {
1037                    log.append(event.clone());
1038                }
1039
1040                let after_append = log.current_index();
1041
1042                // Undo all
1043                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                // Redo all
1053                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            /// Appending after undo should truncate redo history
1064            #[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                // Undo at least one
1076                log.undo();
1077                let index_after_undo = log.current_index();
1078
1079                // Append new event
1080                log.append(new_event);
1081
1082                // Should not be able to redo past the new event
1083                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        // Adding new event should truncate the future
1180        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        // Regression test: navigation after undo should not destroy redo history.
1193        // Standard editors (VS Code, Sublime) allow: type, undo, move around, redo.
1194        let mut log = EventLog::new();
1195
1196        // Type 'a' (Insert + MoveCursor)
1197        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        // Undo (walks back past MoveCursor and Insert)
1214        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        // Navigate (MoveCursor) — should NOT destroy redo
1220        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        // Redo should still work
1235        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        // Write actions after undo SHOULD still clear redo history
1245        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        // New write action should truncate redo
1257        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 for v0.1.77 panic: "range end index 148 out of range for slice of length 125"
1269    ///
1270    /// The bug occurs when:
1271    /// 1. Make many changes (entries grows)
1272    /// 2. mark_saved() sets saved_at_index to current position
1273    /// 3. Undo several times
1274    /// 4. Make new changes - this truncates entries but saved_at_index stays
1275    /// 5. is_at_saved_position() panics on out-of-bounds slice access
1276    #[test]
1277    fn test_is_at_saved_position_after_truncate() {
1278        let mut log = EventLog::new();
1279
1280        // Step 1: Make many changes
1281        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        // Step 2: Save - this sets saved_at_index = 150
1293        log.mark_saved();
1294
1295        // Step 3: Undo 30 times - current_index goes to 120, but entries stay at 150
1296        for _ in 0..30 {
1297            log.undo();
1298        }
1299        assert_eq!(log.current_index(), 120);
1300        assert_eq!(log.entries().len(), 150);
1301
1302        // Step 4: Make new changes - this truncates entries to 120, then adds new
1303        log.append(Event::Insert {
1304            position: 0,
1305            text: "NEW".to_string(),
1306            cursor_id: CursorId(0),
1307        });
1308
1309        // Now entries.len() = 121, but saved_at_index = 150
1310        assert_eq!(log.entries().len(), 121);
1311        assert_eq!(log.current_index(), 121);
1312
1313        // Step 5: Call is_at_saved_position() - THIS PANICS in v0.1.77
1314        // The code does: self.entries[start..end] where end = saved_at_index = 150
1315        // but entries.len() = 121, so 150 is out of bounds
1316        let result = log.is_at_saved_position();
1317
1318        // After fix: should return false (we're not at saved position, we branched off)
1319        assert!(
1320            !result,
1321            "Should not be at saved position after undo + new edit"
1322        );
1323    }
1324}