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