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        /// Row of the status bar in the current frame. Lets the popup
342        /// place its bottom border immediately above the status bar
343        /// regardless of whether the prompt line is visible (which
344        /// shifts the status bar's row by one).
345        status_row: u16,
346    },
347}
348
349/// Margin position for events
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
351pub enum MarginPositionData {
352    Left,
353    Right,
354}
355
356/// Margin content for events
357#[derive(Debug, Clone, Serialize, Deserialize)]
358pub enum MarginContentData {
359    Text(String),
360    Symbol {
361        text: String,
362        color: Option<(u8, u8, u8)>, // RGB color
363    },
364    Empty,
365}
366
367impl Event {
368    /// Returns the inverse event for undo functionality
369    /// Uses UNDO_SENTINEL cursor_id to avoid moving the cursor during undo
370    pub fn inverse(&self) -> Option<Self> {
371        match self {
372            Self::Insert { position, text, .. } => {
373                let range = *position..(position + text.len());
374                Some(Self::Delete {
375                    range,
376                    deleted_text: text.clone(),
377                    cursor_id: CursorId::UNDO_SENTINEL,
378                })
379            }
380            Self::Delete {
381                range,
382                deleted_text,
383                ..
384            } => Some(Self::Insert {
385                position: range.start,
386                text: deleted_text.clone(),
387                cursor_id: CursorId::UNDO_SENTINEL,
388            }),
389            Self::Batch {
390                events,
391                description,
392            } => {
393                // Invert all events in the batch in reverse order
394                let inverted: Option<Vec<Self>> =
395                    events.iter().rev().map(|e| e.inverse()).collect();
396
397                inverted.map(|inverted_events| Self::Batch {
398                    events: inverted_events,
399                    description: format!("Undo: {}", description),
400                })
401            }
402            Self::AddCursor {
403                cursor_id,
404                position,
405                anchor,
406            } => {
407                // To undo adding a cursor, we remove it (store its state for redo)
408                Some(Self::RemoveCursor {
409                    cursor_id: *cursor_id,
410                    position: *position,
411                    anchor: *anchor,
412                })
413            }
414            Self::RemoveCursor {
415                cursor_id,
416                position,
417                anchor,
418            } => {
419                // To undo removing a cursor, we add it back
420                Some(Self::AddCursor {
421                    cursor_id: *cursor_id,
422                    position: *position,
423                    anchor: *anchor,
424                })
425            }
426            Self::MoveCursor {
427                cursor_id,
428                old_position,
429                new_position,
430                old_anchor,
431                new_anchor,
432                old_sticky_column,
433                new_sticky_column,
434            } => {
435                // Invert by swapping old and new positions
436                Some(Self::MoveCursor {
437                    cursor_id: *cursor_id,
438                    old_position: *new_position,
439                    new_position: *old_position,
440                    old_anchor: *new_anchor,
441                    new_anchor: *old_anchor,
442                    old_sticky_column: *new_sticky_column,
443                    new_sticky_column: *old_sticky_column,
444                })
445            }
446            Self::AddOverlay { .. } => {
447                // Overlays are ephemeral decorations, not undoable
448                None
449            }
450            Self::RemoveOverlay { .. } => {
451                // Overlays are ephemeral decorations, not undoable
452                None
453            }
454            Self::ClearNamespace { .. } => {
455                // Overlays are ephemeral decorations, not undoable
456                None
457            }
458            Self::Scroll { line_offset } => Some(Self::Scroll {
459                line_offset: -line_offset,
460            }),
461            Self::SetViewport { top_line: _ } => {
462                // Can't invert without knowing old top_line
463                None
464            }
465            Self::ChangeMode { mode: _ } => {
466                // Can't invert without knowing old mode
467                None
468            }
469            Self::BulkEdit {
470                old_snapshot,
471                new_snapshot,
472                old_cursors,
473                new_cursors,
474                description,
475                edits,
476                displaced_markers,
477            } => {
478                // Inverse swaps both snapshots, cursor states, and edit directions.
479                // Swapping del_len/ins_len makes undo apply reverse marker adjustments.
480                let inverted_edits: Vec<(usize, usize, usize)> = edits
481                    .iter()
482                    .map(|(pos, del_len, ins_len)| (*pos, *ins_len, *del_len))
483                    .collect();
484
485                Some(Self::BulkEdit {
486                    old_snapshot: new_snapshot.clone(),
487                    new_snapshot: old_snapshot.clone(),
488                    old_cursors: new_cursors.clone(),
489                    new_cursors: old_cursors.clone(),
490                    description: format!("Undo: {}", description),
491                    edits: inverted_edits,
492                    // displaced_markers only applies to undo (restoring original positions).
493                    // The redo direction doesn't need them — forward adjustments are correct.
494                    // We pass them through so undo can use them.
495                    displaced_markers: displaced_markers.clone(),
496                })
497            }
498            // Other events (popups, margins, splits, etc.) are not automatically invertible
499            _ => None,
500        }
501    }
502
503    /// Returns true if this event modifies the buffer content
504    pub fn modifies_buffer(&self) -> bool {
505        match self {
506            Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
507            Self::Batch { events, .. } => events.iter().any(|e| e.modifies_buffer()),
508            _ => false,
509        }
510    }
511
512    /// Returns true if this event is a write action (modifies state in a way that should be undoable)
513    /// Returns false for readonly actions like cursor movement, scrolling, viewport changes, etc.
514    ///
515    /// Write actions include:
516    /// - Buffer modifications (Insert, Delete)
517    /// - Cursor structure changes (AddCursor, RemoveCursor)
518    /// - Batches containing write actions
519    ///
520    /// Readonly actions include:
521    /// - Cursor movement (MoveCursor)
522    /// - Scrolling and viewport changes (Scroll, SetViewport)
523    /// - UI events (overlays, popups, margins, mode changes, etc.)
524    pub fn is_write_action(&self) -> bool {
525        match self {
526            // Buffer modifications are write actions
527            Self::Insert { .. } | Self::Delete { .. } | Self::BulkEdit { .. } => true,
528
529            // Adding/removing cursors are write actions (structural changes)
530            Self::AddCursor { .. } | Self::RemoveCursor { .. } => true,
531
532            // Batches are write actions if they contain any write actions
533            Self::Batch { events, .. } => events.iter().any(|e| e.is_write_action()),
534
535            // All other events are readonly (movement, scrolling, UI, etc.)
536            _ => false,
537        }
538    }
539
540    /// Returns the cursor ID associated with this event, if any
541    pub fn cursor_id(&self) -> Option<CursorId> {
542        match self {
543            Self::Insert { cursor_id, .. }
544            | Self::Delete { cursor_id, .. }
545            | Self::MoveCursor { cursor_id, .. }
546            | Self::AddCursor { cursor_id, .. }
547            | Self::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
548            _ => None,
549        }
550    }
551}
552
553/// A log entry containing an event and metadata
554#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct LogEntry {
556    /// The event
557    pub event: Event,
558
559    /// Timestamp when the event occurred (milliseconds since epoch)
560    pub timestamp: u64,
561
562    /// Optional description for debugging
563    pub description: Option<String>,
564
565    /// Markers displaced by deletions in this event.
566    /// Stored as (marker_id_raw, original_byte_position).
567    /// When this event is undone, the inverse Insert restores these markers
568    /// to their exact original positions.
569    #[serde(default, skip_serializing_if = "Vec::is_empty")]
570    pub displaced_markers: Vec<(u64, usize)>,
571
572    /// Undo-group id. Entries sharing the same id are undone/redone as a
573    /// single atomic unit (e.g. a whole macro replay). `None` means the entry
574    /// stands on its own. Runtime-only — not persisted, so reloaded logs fall
575    /// back to per-entry undo.
576    #[serde(skip)]
577    pub group_id: Option<u64>,
578}
579
580impl LogEntry {
581    pub fn new(event: Event) -> Self {
582        Self {
583            event,
584            timestamp: std::time::SystemTime::now()
585                .duration_since(std::time::UNIX_EPOCH)
586                .unwrap()
587                .as_millis() as u64,
588            description: None,
589            displaced_markers: Vec::new(),
590            group_id: None,
591        }
592    }
593
594    pub fn with_description(mut self, description: String) -> Self {
595        self.description = Some(description);
596        self
597    }
598}
599
600/// Snapshot of editor state for fast undo/redo
601#[derive(Debug, Clone)]
602pub struct Snapshot {
603    /// Index in the event log where this snapshot was taken
604    pub log_index: usize,
605
606    /// Buffer content at this point (stored as ChunkTree reference)
607    /// For now we'll use a placeholder - will be filled in when we implement Buffer
608    pub buffer_state: (),
609
610    /// Cursor positions at this point
611    pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
612}
613
614/// The event log - append-only log of all events
615pub struct EventLog {
616    /// All logged events
617    entries: Vec<LogEntry>,
618
619    /// Current position in the log (for undo/redo)
620    current_index: usize,
621
622    /// Periodic snapshots for fast seeking
623    snapshots: Vec<Snapshot>,
624
625    /// How often to create snapshots (every N events)
626    snapshot_interval: usize,
627
628    /// Optional file for streaming events to disk (runtime only)
629    #[cfg(feature = "runtime")]
630    stream_file: Option<std::fs::File>,
631
632    /// Index at which the buffer was last saved (for tracking modified status)
633    /// When current_index equals saved_at_index, the buffer is not modified
634    saved_at_index: Option<usize>,
635
636    /// Monotonic allocator for undo-group ids.
637    next_group_id: u64,
638
639    /// Id tagged onto appended entries while an undo group is open.
640    current_group: Option<u64>,
641
642    /// Nesting depth of open undo groups. The group is closed (and a fresh id
643    /// allocated for the next group) only when this returns to zero.
644    group_depth: u32,
645}
646
647impl EventLog {
648    /// Create a new empty event log
649    pub fn new() -> Self {
650        Self {
651            entries: Vec::new(),
652            current_index: 0,
653            snapshots: Vec::new(),
654            snapshot_interval: 100,
655            #[cfg(feature = "runtime")]
656            stream_file: None,
657            saved_at_index: Some(0), // New buffer starts at "saved" state (index 0)
658            next_group_id: 0,
659            current_group: None,
660            group_depth: 0,
661        }
662    }
663
664    /// Open an undo group. All write actions appended until the matching
665    /// [`Self::end_undo_group`] are reverted/reapplied together as one unit.
666    /// Nestable: only the outermost begin/end pair establishes the group.
667    pub fn begin_undo_group(&mut self) {
668        if self.group_depth == 0 {
669            self.current_group = Some(self.next_group_id);
670            self.next_group_id += 1;
671        }
672        self.group_depth += 1;
673    }
674
675    /// Close the undo group opened by [`Self::begin_undo_group`].
676    pub fn end_undo_group(&mut self) {
677        if self.group_depth > 0 {
678            self.group_depth -= 1;
679            if self.group_depth == 0 {
680                self.current_group = None;
681            }
682        }
683    }
684
685    /// Mark the current position as the saved point
686    /// Call this when the buffer is saved to disk
687    pub fn mark_saved(&mut self) {
688        self.saved_at_index = Some(self.current_index);
689    }
690
691    /// Invalidate the saved position so the buffer is always considered modified.
692    /// Call this after hot exit recovery, where the buffer content differs from
693    /// disk but the event log has no record of the changes.
694    pub fn clear_saved_position(&mut self) {
695        self.saved_at_index = None;
696    }
697
698    /// Check if the buffer is at the saved position (not modified)
699    /// Returns true if we're at the saved position OR if all events between
700    /// saved_at_index and current_index are readonly (don't modify buffer content)
701    pub fn is_at_saved_position(&self) -> bool {
702        match self.saved_at_index {
703            None => false,
704            Some(saved_idx) if saved_idx == self.current_index => true,
705            Some(saved_idx) => {
706                // Check if all events between saved position and current position
707                // are readonly (don't modify buffer content)
708                let (start, end) = if saved_idx < self.current_index {
709                    (saved_idx, self.current_index)
710                } else {
711                    (self.current_index, saved_idx)
712                };
713
714                // All events in range [start, end) must be readonly
715                self.entries[start..end]
716                    .iter()
717                    .all(|entry| !entry.event.modifies_buffer())
718            }
719        }
720    }
721
722    /// Enable streaming events to a file (runtime only)
723    #[cfg(feature = "runtime")]
724    pub fn enable_streaming<P: AsRef<std::path::Path>>(&mut self, path: P) -> std::io::Result<()> {
725        use std::io::Write;
726
727        let mut file = std::fs::OpenOptions::new()
728            .create(true)
729            .write(true)
730            .truncate(true)
731            .open(path)?;
732
733        // Write header
734        writeln!(file, "# Event Log Stream")?;
735        writeln!(file, "# Started at: {}", chrono::Local::now())?;
736        writeln!(file, "# Format: JSON Lines (one event per line)")?;
737        writeln!(file, "#")?;
738
739        self.stream_file = Some(file);
740        Ok(())
741    }
742
743    /// Disable streaming (runtime only)
744    #[cfg(feature = "runtime")]
745    pub fn disable_streaming(&mut self) {
746        self.stream_file = None;
747    }
748
749    /// Log rendering state (for debugging, runtime only)
750    #[cfg(feature = "runtime")]
751    pub fn log_render_state(
752        &mut self,
753        cursor_pos: usize,
754        screen_cursor_x: u16,
755        screen_cursor_y: u16,
756        buffer_len: usize,
757    ) {
758        if let Some(ref mut file) = self.stream_file {
759            use std::io::Write;
760
761            let render_info = serde_json::json!({
762                "type": "render",
763                "timestamp": chrono::Local::now().to_rfc3339(),
764                "cursor_position": cursor_pos,
765                "screen_cursor": {"x": screen_cursor_x, "y": screen_cursor_y},
766                "buffer_length": buffer_len,
767            });
768
769            if let Err(e) = writeln!(file, "{render_info}") {
770                tracing::trace!("Warning: Failed to write render info to stream: {e}");
771            }
772            if let Err(e) = file.flush() {
773                tracing::trace!("Warning: Failed to flush event stream: {e}");
774            }
775        }
776    }
777
778    /// Log keystroke (for debugging, runtime only)
779    #[cfg(feature = "runtime")]
780    pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
781        if let Some(ref mut file) = self.stream_file {
782            use std::io::Write;
783
784            let keystroke_info = serde_json::json!({
785                "type": "keystroke",
786                "timestamp": chrono::Local::now().to_rfc3339(),
787                "key": key_code,
788                "modifiers": modifiers,
789            });
790
791            if let Err(e) = writeln!(file, "{keystroke_info}") {
792                tracing::trace!("Warning: Failed to write keystroke to stream: {e}");
793            }
794            if let Err(e) = file.flush() {
795                tracing::trace!("Warning: Failed to flush event stream: {e}");
796            }
797        }
798    }
799
800    /// Append an event to the log
801    pub fn append(&mut self, event: Event) -> usize {
802        // When redo history exists (after undo), only write actions are logged.
803        // Non-write events (MoveCursor, Scroll, etc.) are still applied to the
804        // editor state but not recorded in the log, preserving redo history.
805        // This matches standard editor behavior (VS Code, Sublime, etc.) where
806        // navigation after undo does not destroy the redo chain.
807        if self.current_index < self.entries.len() {
808            if event.is_write_action() {
809                // Write action: truncate redo history and log normally
810                self.entries.truncate(self.current_index);
811
812                // Invalidate saved_at_index if it pointed to a truncated entry
813                if let Some(saved_idx) = self.saved_at_index {
814                    if saved_idx > self.current_index {
815                        self.saved_at_index = None;
816                    }
817                }
818            } else {
819                // Non-write event while redo exists: skip logging to preserve redo
820                return self.current_index;
821            }
822        }
823
824        // Stream event to file if enabled (runtime only)
825        #[cfg(feature = "runtime")]
826        if let Some(ref mut file) = self.stream_file {
827            use std::io::Write;
828
829            let stream_entry = serde_json::json!({
830                "index": self.entries.len(),
831                "timestamp": chrono::Local::now().to_rfc3339(),
832                "event": event,
833            });
834
835            // Write JSON line and flush immediately for real-time logging
836            if let Err(e) = writeln!(file, "{stream_entry}") {
837                tracing::trace!("Warning: Failed to write to event stream: {e}");
838            }
839            if let Err(e) = file.flush() {
840                tracing::trace!("Warning: Failed to flush event stream: {e}");
841            }
842        }
843
844        let mut entry = LogEntry::new(event);
845        entry.group_id = self.current_group;
846        self.entries.push(entry);
847        self.current_index = self.entries.len();
848
849        // Check if we should create a snapshot
850        if self.entries.len().is_multiple_of(self.snapshot_interval) {
851            // Snapshot creation will be implemented when we have Buffer
852            // For now, just track that we'd create one here
853        }
854
855        self.current_index - 1
856    }
857
858    /// Set displaced markers on the last appended entry.
859    /// Call this right after `append()` to record markers that were inside
860    /// the deleted range, so undo can restore them to exact positions.
861    pub fn set_displaced_markers_on_last(&mut self, markers: Vec<(u64, usize)>) {
862        if let Some(entry) = self.entries.last_mut() {
863            entry.displaced_markers = markers;
864        }
865    }
866
867    /// Get the current event index
868    pub fn current_index(&self) -> usize {
869        self.current_index
870    }
871
872    /// Get the number of events in the log
873    pub fn len(&self) -> usize {
874        self.entries.len()
875    }
876
877    /// Check if the event log is empty
878    pub fn is_empty(&self) -> bool {
879        self.entries.is_empty()
880    }
881
882    /// Can we undo?
883    pub fn can_undo(&self) -> bool {
884        self.current_index > 0
885    }
886
887    /// Can we redo?
888    pub fn can_redo(&self) -> bool {
889        self.current_index < self.entries.len()
890    }
891
892    /// Move back through events (for undo)
893    /// Collects all events up to and including the first write action, returns their inverses.
894    /// Each inverse event is paired with displaced markers from the original event,
895    /// which should be restored after applying the inverse Insert.
896    /// This processes readonly events (like scrolling) and stops at write events (like Insert/Delete)
897    pub fn undo(&mut self) -> Vec<(Event, Vec<(u64, usize)>)> {
898        let mut inverse_events = Vec::new();
899        let mut found_write_action = false;
900        // Group id of the write action that opened this undo unit, if any.
901        // While set, the walk keeps consuming entries belonging to the same
902        // group so a whole grouped edit (e.g. a macro replay) reverts at once.
903        let mut group: Option<u64> = None;
904
905        while self.can_undo() {
906            let idx = self.current_index - 1;
907            let is_write = self.entries[idx].event.is_write_action();
908            let entry_group = self.entries[idx].group_id;
909
910            if found_write_action {
911                match group {
912                    // Ungrouped edit: stop after the first write action.
913                    None => break,
914                    // Grouped edit: stop at the first entry outside the group.
915                    Some(g) if entry_group != Some(g) => break,
916                    Some(_) => {}
917                }
918            }
919
920            self.current_index = idx;
921
922            if is_write && !found_write_action {
923                found_write_action = true;
924                group = entry_group;
925            }
926
927            // Try to get the inverse of this event
928            if let Some(inverse) = self.entries[idx].event.inverse() {
929                inverse_events.push((inverse, self.entries[idx].displaced_markers.clone()));
930            }
931            // If no inverse exists (like MoveCursor), we just skip it
932        }
933
934        inverse_events
935    }
936
937    /// Move forward through events (for redo)
938    /// Collects the first write action plus all readonly events after it (until next write action)
939    /// This processes readonly events (like scrolling) with write events (like Insert/Delete)
940    pub fn redo(&mut self) -> Vec<Event> {
941        let mut events = Vec::new();
942        let mut found_write_action = false;
943        // Mirror of `undo`'s grouping: once a grouped write action is reached,
944        // keep reapplying entries from the same group as one unit.
945        let mut group: Option<u64> = None;
946
947        // Keep moving forward to collect write action and subsequent readonly events
948        while self.can_redo() {
949            let idx = self.current_index;
950            let is_write = self.entries[idx].event.is_write_action();
951            let entry_group = self.entries[idx].group_id;
952
953            // If we've already found a write action and this is another write action, stop
954            // (unless it belongs to the same open undo group).
955            if found_write_action && is_write {
956                match group {
957                    None => break,
958                    Some(g) if entry_group != Some(g) => break,
959                    Some(_) => {}
960                }
961            }
962
963            if is_write && !found_write_action {
964                found_write_action = true;
965                group = entry_group;
966            }
967
968            events.push(self.entries[idx].event.clone());
969            self.current_index = idx + 1;
970        }
971
972        events
973    }
974
975    /// Get all events from the log
976    pub fn entries(&self) -> &[LogEntry] {
977        &self.entries
978    }
979
980    /// Get events in a range
981    pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
982        &self.entries[range]
983    }
984
985    /// Get the most recent event
986    pub fn last_event(&self) -> Option<&Event> {
987        if self.current_index > 0 {
988            Some(&self.entries[self.current_index - 1].event)
989        } else {
990            None
991        }
992    }
993
994    /// Clear all events (for testing or reset)
995    pub fn clear(&mut self) {
996        self.entries.clear();
997        self.current_index = 0;
998        self.snapshots.clear();
999    }
1000
1001    /// Save event log to JSON Lines format
1002    pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
1003        use std::io::Write;
1004        let file = std::fs::File::create(path)?;
1005        let mut writer = std::io::BufWriter::new(file);
1006
1007        for entry in &self.entries {
1008            let json = serde_json::to_string(entry)?;
1009            writeln!(writer, "{json}")?;
1010        }
1011
1012        Ok(())
1013    }
1014
1015    /// Load event log from JSON Lines format
1016    pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
1017        use std::io::BufRead;
1018        let file = std::fs::File::open(path)?;
1019        let reader = std::io::BufReader::new(file);
1020
1021        let mut log = Self::new();
1022
1023        for line in reader.lines() {
1024            let line = line?;
1025            if line.trim().is_empty() {
1026                continue;
1027            }
1028            let entry: LogEntry = serde_json::from_str(&line)?;
1029            log.entries.push(entry);
1030        }
1031
1032        log.current_index = log.entries.len();
1033
1034        Ok(log)
1035    }
1036
1037    /// Set snapshot interval
1038    pub fn set_snapshot_interval(&mut self, interval: usize) {
1039        self.snapshot_interval = interval;
1040    }
1041}
1042
1043impl Default for EventLog {
1044    fn default() -> Self {
1045        Self::new()
1046    }
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051    use super::*;
1052
1053    // Property-based tests
1054    #[cfg(test)]
1055    mod property_tests {
1056        use super::*;
1057        use proptest::prelude::*;
1058
1059        /// Helper to generate random events
1060        fn arb_event() -> impl Strategy<Value = Event> {
1061            prop_oneof![
1062                // Insert events
1063                (0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
1064                    position: pos,
1065                    text,
1066                    cursor_id: CursorId(0),
1067                }),
1068                // Delete events
1069                (0usize..1000, 1usize..50).prop_map(|(pos, len)| Event::Delete {
1070                    range: pos..pos + len,
1071                    deleted_text: "x".repeat(len),
1072                    cursor_id: CursorId(0),
1073                }),
1074            ]
1075        }
1076
1077        proptest! {
1078            /// Event inverse should be truly inverse
1079            #[test]
1080            fn event_inverse_property(event in arb_event()) {
1081                if let Some(inverse) = event.inverse() {
1082                    // The inverse of an inverse should be the original
1083                    // (for commutative operations)
1084                    if let Some(double_inverse) = inverse.inverse() {
1085                        match (&event, &double_inverse) {
1086                            (Event::Insert { position: p1, text: t1, .. },
1087                             Event::Insert { position: p2, text: t2, .. }) => {
1088                                assert_eq!(p1, p2);
1089                                assert_eq!(t1, t2);
1090                            }
1091                            (Event::Delete { range: r1, deleted_text: dt1, .. },
1092                             Event::Delete { range: r2, deleted_text: dt2, .. }) => {
1093                                assert_eq!(r1, r2);
1094                                assert_eq!(dt1, dt2);
1095                            }
1096                            _ => {}
1097                        }
1098                    }
1099                }
1100            }
1101
1102            /// Undo then redo should restore state
1103            #[test]
1104            fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
1105                let mut log = EventLog::new();
1106
1107                // Append all events
1108                for event in &events {
1109                    log.append(event.clone());
1110                }
1111
1112                let after_append = log.current_index();
1113
1114                // Undo all
1115                let mut undo_count = 0;
1116                while log.can_undo() {
1117                    log.undo();
1118                    undo_count += 1;
1119                }
1120
1121                assert_eq!(log.current_index(), 0);
1122                assert_eq!(undo_count, events.len());
1123
1124                // Redo all
1125                let mut redo_count = 0;
1126                while log.can_redo() {
1127                    log.redo();
1128                    redo_count += 1;
1129                }
1130
1131                assert_eq!(log.current_index(), after_append);
1132                assert_eq!(redo_count, events.len());
1133            }
1134
1135            /// Appending after undo should truncate redo history
1136            #[test]
1137            fn append_after_undo_truncates(
1138                initial_events in prop::collection::vec(arb_event(), 2..10),
1139                new_event in arb_event()
1140            ) {
1141                let mut log = EventLog::new();
1142
1143                for event in &initial_events {
1144                    log.append(event.clone());
1145                }
1146
1147                // Undo at least one
1148                log.undo();
1149                let index_after_undo = log.current_index();
1150
1151                // Append new event
1152                log.append(new_event);
1153
1154                // Should not be able to redo past the new event
1155                assert_eq!(log.current_index(), index_after_undo + 1);
1156                assert!(!log.can_redo());
1157            }
1158        }
1159    }
1160
1161    #[test]
1162    fn test_event_log_append() {
1163        let mut log = EventLog::new();
1164        let event = Event::Insert {
1165            position: 0,
1166            text: "hello".to_string(),
1167            cursor_id: CursorId(0),
1168        };
1169
1170        let index = log.append(event);
1171        assert_eq!(index, 0);
1172        assert_eq!(log.current_index(), 1);
1173        assert_eq!(log.entries().len(), 1);
1174    }
1175
1176    #[test]
1177    fn test_undo_redo() {
1178        let mut log = EventLog::new();
1179
1180        log.append(Event::Insert {
1181            position: 0,
1182            text: "a".to_string(),
1183            cursor_id: CursorId(0),
1184        });
1185
1186        log.append(Event::Insert {
1187            position: 1,
1188            text: "b".to_string(),
1189            cursor_id: CursorId(0),
1190        });
1191
1192        assert_eq!(log.current_index(), 2);
1193        assert!(log.can_undo());
1194        assert!(!log.can_redo());
1195
1196        log.undo();
1197        assert_eq!(log.current_index(), 1);
1198        assert!(log.can_undo());
1199        assert!(log.can_redo());
1200
1201        log.undo();
1202        assert_eq!(log.current_index(), 0);
1203        assert!(!log.can_undo());
1204        assert!(log.can_redo());
1205
1206        log.redo();
1207        assert_eq!(log.current_index(), 1);
1208    }
1209
1210    #[test]
1211    fn test_event_inverse() {
1212        let insert = Event::Insert {
1213            position: 5,
1214            text: "hello".to_string(),
1215            cursor_id: CursorId(0),
1216        };
1217
1218        let inverse = insert.inverse().unwrap();
1219        match inverse {
1220            Event::Delete {
1221                range,
1222                deleted_text,
1223                ..
1224            } => {
1225                assert_eq!(range, 5..10);
1226                assert_eq!(deleted_text, "hello");
1227            }
1228            _ => panic!("Expected Delete event"),
1229        }
1230    }
1231
1232    #[test]
1233    fn test_truncate_on_new_event_after_undo() {
1234        let mut log = EventLog::new();
1235
1236        log.append(Event::Insert {
1237            position: 0,
1238            text: "a".to_string(),
1239            cursor_id: CursorId(0),
1240        });
1241
1242        log.append(Event::Insert {
1243            position: 1,
1244            text: "b".to_string(),
1245            cursor_id: CursorId(0),
1246        });
1247
1248        log.undo();
1249        assert_eq!(log.entries().len(), 2);
1250
1251        // Adding new event should truncate the future
1252        log.append(Event::Insert {
1253            position: 1,
1254            text: "c".to_string(),
1255            cursor_id: CursorId(0),
1256        });
1257
1258        assert_eq!(log.entries().len(), 2);
1259        assert_eq!(log.current_index(), 2);
1260    }
1261
1262    #[test]
1263    fn test_navigation_after_undo_preserves_redo() {
1264        // Regression test: navigation after undo should not destroy redo history.
1265        // Standard editors (VS Code, Sublime) allow: type, undo, move around, redo.
1266        let mut log = EventLog::new();
1267
1268        // Type 'a' (Insert + MoveCursor)
1269        log.append(Event::Insert {
1270            position: 0,
1271            text: "a".to_string(),
1272            cursor_id: CursorId(0),
1273        });
1274        log.append(Event::MoveCursor {
1275            cursor_id: CursorId(0),
1276            old_position: 0,
1277            new_position: 1,
1278            old_anchor: None,
1279            new_anchor: None,
1280            old_sticky_column: 0,
1281            new_sticky_column: 0,
1282        });
1283        assert_eq!(log.current_index(), 2);
1284
1285        // Undo (walks back past MoveCursor and Insert)
1286        let undo_events = log.undo();
1287        assert!(!undo_events.is_empty());
1288        assert_eq!(log.current_index(), 0);
1289        assert!(log.can_redo());
1290
1291        // Navigate (MoveCursor) — should NOT destroy redo
1292        log.append(Event::MoveCursor {
1293            cursor_id: CursorId(0),
1294            old_position: 0,
1295            new_position: 0,
1296            old_anchor: None,
1297            new_anchor: None,
1298            old_sticky_column: 0,
1299            new_sticky_column: 0,
1300        });
1301        assert!(
1302            log.can_redo(),
1303            "Navigation after undo should preserve redo history"
1304        );
1305
1306        // Redo should still work
1307        let redo_events = log.redo();
1308        assert!(
1309            !redo_events.is_empty(),
1310            "Redo should return events after navigation"
1311        );
1312    }
1313
1314    #[test]
1315    fn test_undo_group_reverts_and_reapplies_atomically() {
1316        // Three grouped inserts (e.g. a macro replay) collapse into one
1317        // undo/redo unit (#2062).
1318        let mut log = EventLog::new();
1319
1320        log.begin_undo_group();
1321        for (i, ch) in ['a', 'b', 'c'].into_iter().enumerate() {
1322            log.append(Event::Insert {
1323                position: i,
1324                text: ch.to_string(),
1325                cursor_id: CursorId(0),
1326            });
1327        }
1328        log.end_undo_group();
1329        assert_eq!(log.current_index(), 3);
1330
1331        // One undo reverts the whole group.
1332        let undo_events = log.undo();
1333        assert_eq!(undo_events.len(), 3, "all three inserts revert in one undo");
1334        assert_eq!(log.current_index(), 0);
1335        assert!(!log.can_undo());
1336
1337        // One redo reapplies the whole group.
1338        let redo_events = log.redo();
1339        assert_eq!(
1340            redo_events.len(),
1341            3,
1342            "all three inserts reapply in one redo"
1343        );
1344        assert_eq!(log.current_index(), 3);
1345        assert!(!log.can_redo());
1346    }
1347
1348    #[test]
1349    fn test_ungrouped_edit_after_group_undoes_separately() {
1350        // A standalone write after a group must NOT merge with it: the first
1351        // undo reverts only the standalone edit, the second reverts the group.
1352        let mut log = EventLog::new();
1353
1354        log.begin_undo_group();
1355        log.append(Event::Insert {
1356            position: 0,
1357            text: "ab".to_string(),
1358            cursor_id: CursorId(0),
1359        });
1360        log.append(Event::Insert {
1361            position: 2,
1362            text: "cd".to_string(),
1363            cursor_id: CursorId(0),
1364        });
1365        log.end_undo_group();
1366
1367        // Standalone insert (not in any group).
1368        log.append(Event::Insert {
1369            position: 4,
1370            text: "Z".to_string(),
1371            cursor_id: CursorId(0),
1372        });
1373        assert_eq!(log.current_index(), 3);
1374
1375        // First undo: only the standalone "Z".
1376        let first = log.undo();
1377        assert_eq!(first.len(), 1);
1378        assert_eq!(log.current_index(), 2);
1379
1380        // Second undo: the whole group ("ab" + "cd").
1381        let second = log.undo();
1382        assert_eq!(second.len(), 2);
1383        assert_eq!(log.current_index(), 0);
1384    }
1385
1386    #[test]
1387    fn test_consecutive_groups_undo_independently() {
1388        // Two back-to-back groups get distinct ids and undo as separate units.
1389        let mut log = EventLog::new();
1390
1391        for _ in 0..2 {
1392            log.begin_undo_group();
1393            log.append(Event::Insert {
1394                position: 0,
1395                text: "xy".to_string(),
1396                cursor_id: CursorId(0),
1397            });
1398            log.append(Event::Insert {
1399                position: 2,
1400                text: "zw".to_string(),
1401                cursor_id: CursorId(0),
1402            });
1403            log.end_undo_group();
1404        }
1405        assert_eq!(log.current_index(), 4);
1406
1407        let first = log.undo();
1408        assert_eq!(first.len(), 2, "second group reverts on its own");
1409        assert_eq!(log.current_index(), 2);
1410
1411        let second = log.undo();
1412        assert_eq!(second.len(), 2, "first group reverts on its own");
1413        assert_eq!(log.current_index(), 0);
1414    }
1415
1416    #[test]
1417    fn test_write_action_after_undo_clears_redo() {
1418        // Write actions after undo SHOULD still clear redo history
1419        let mut log = EventLog::new();
1420
1421        log.append(Event::Insert {
1422            position: 0,
1423            text: "a".to_string(),
1424            cursor_id: CursorId(0),
1425        });
1426
1427        log.undo();
1428        assert!(log.can_redo());
1429
1430        // New write action should truncate redo
1431        log.append(Event::Insert {
1432            position: 0,
1433            text: "b".to_string(),
1434            cursor_id: CursorId(0),
1435        });
1436        assert!(
1437            !log.can_redo(),
1438            "Write action after undo should clear redo history"
1439        );
1440    }
1441
1442    /// Test for v0.1.77 panic: "range end index 148 out of range for slice of length 125"
1443    ///
1444    /// The bug occurs when:
1445    /// 1. Make many changes (entries grows)
1446    /// 2. mark_saved() sets saved_at_index to current position
1447    /// 3. Undo several times
1448    /// 4. Make new changes - this truncates entries but saved_at_index stays
1449    /// 5. is_at_saved_position() panics on out-of-bounds slice access
1450    #[test]
1451    fn test_is_at_saved_position_after_truncate() {
1452        let mut log = EventLog::new();
1453
1454        // Step 1: Make many changes
1455        for i in 0..150 {
1456            log.append(Event::Insert {
1457                position: i,
1458                text: "x".to_string(),
1459                cursor_id: CursorId(0),
1460            });
1461        }
1462
1463        assert_eq!(log.entries().len(), 150);
1464        assert_eq!(log.current_index(), 150);
1465
1466        // Step 2: Save - this sets saved_at_index = 150
1467        log.mark_saved();
1468
1469        // Step 3: Undo 30 times - current_index goes to 120, but entries stay at 150
1470        for _ in 0..30 {
1471            log.undo();
1472        }
1473        assert_eq!(log.current_index(), 120);
1474        assert_eq!(log.entries().len(), 150);
1475
1476        // Step 4: Make new changes - this truncates entries to 120, then adds new
1477        log.append(Event::Insert {
1478            position: 0,
1479            text: "NEW".to_string(),
1480            cursor_id: CursorId(0),
1481        });
1482
1483        // Now entries.len() = 121, but saved_at_index = 150
1484        assert_eq!(log.entries().len(), 121);
1485        assert_eq!(log.current_index(), 121);
1486
1487        // Step 5: Call is_at_saved_position() - THIS PANICS in v0.1.77
1488        // The code does: self.entries[start..end] where end = saved_at_index = 150
1489        // but entries.len() = 121, so 150 is out of bounds
1490        let result = log.is_at_saved_position();
1491
1492        // After fix: should return false (we're not at saved position, we branched off)
1493        assert!(
1494            !result,
1495            "Should not be at saved position after undo + new edit"
1496        );
1497    }
1498}