use crate::view::overlay::{OverlayHandle, OverlayNamespace};
use serde::{Deserialize, Serialize};
use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CursorId(pub usize);
impl CursorId {
pub const UNDO_SENTINEL: CursorId = CursorId(usize::MAX);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SplitId(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BufferId(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SplitDirection {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Event {
Insert {
position: usize,
text: String,
cursor_id: CursorId,
},
Delete {
range: Range<usize>,
deleted_text: String,
cursor_id: CursorId,
},
MoveCursor {
cursor_id: CursorId,
old_position: usize,
new_position: usize,
old_anchor: Option<usize>,
new_anchor: Option<usize>,
old_sticky_column: usize,
new_sticky_column: usize,
},
AddCursor {
cursor_id: CursorId,
position: usize,
anchor: Option<usize>,
},
RemoveCursor {
cursor_id: CursorId,
position: usize,
anchor: Option<usize>,
},
Scroll {
line_offset: isize,
},
SetViewport {
top_line: usize,
},
Recenter,
SetAnchor {
cursor_id: CursorId,
position: usize,
},
ClearAnchor {
cursor_id: CursorId,
},
ChangeMode {
mode: String,
},
AddOverlay {
namespace: Option<OverlayNamespace>,
range: Range<usize>,
face: OverlayFace,
priority: i32,
message: Option<String>,
},
RemoveOverlay {
handle: OverlayHandle,
},
RemoveOverlaysInRange {
range: Range<usize>,
},
ClearNamespace {
namespace: OverlayNamespace,
},
ClearOverlays,
ShowPopup {
popup: PopupData,
},
HidePopup,
ClearPopups,
PopupSelectNext,
PopupSelectPrev,
PopupPageDown,
PopupPageUp,
AddMarginAnnotation {
line: usize,
position: MarginPositionData,
content: MarginContentData,
annotation_id: Option<String>,
},
RemoveMarginAnnotation {
annotation_id: String,
},
RemoveMarginAnnotationsAtLine {
line: usize,
position: MarginPositionData,
},
ClearMarginPosition {
position: MarginPositionData,
},
ClearMargins,
SetLineNumbers {
enabled: bool,
},
SplitPane {
direction: SplitDirection,
new_buffer_id: BufferId,
ratio: f32,
},
CloseSplit {
split_id: SplitId,
},
SetActiveSplit {
split_id: SplitId,
},
AdjustSplitRatio {
split_id: SplitId,
delta: f32,
},
NextSplit,
PrevSplit,
Batch {
events: Vec<Event>,
description: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OverlayFace {
Underline {
color: (u8, u8, u8), style: UnderlineStyle,
},
Background {
color: (u8, u8, u8),
},
Foreground {
color: (u8, u8, u8),
},
Style {
color: (u8, u8, u8),
bold: bool,
italic: bool,
underline: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum UnderlineStyle {
Straight,
Wavy,
Dotted,
Dashed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopupData {
pub title: Option<String>,
pub content: PopupContentData,
pub position: PopupPositionData,
pub width: u16,
pub max_height: u16,
pub bordered: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PopupContentData {
Text(Vec<String>),
List {
items: Vec<PopupListItemData>,
selected: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PopupListItemData {
pub text: String,
pub detail: Option<String>,
pub icon: Option<String>,
pub data: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PopupPositionData {
AtCursor,
BelowCursor,
AboveCursor,
Fixed { x: u16, y: u16 },
Centered,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MarginPositionData {
Left,
Right,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MarginContentData {
Text(String),
Symbol {
text: String,
color: Option<(u8, u8, u8)>, },
Empty,
}
impl Event {
pub fn inverse(&self) -> Option<Event> {
match self {
Event::Insert { position, text, .. } => {
let range = *position..(position + text.len());
Some(Event::Delete {
range,
deleted_text: text.clone(),
cursor_id: CursorId::UNDO_SENTINEL,
})
}
Event::Delete {
range,
deleted_text,
..
} => Some(Event::Insert {
position: range.start,
text: deleted_text.clone(),
cursor_id: CursorId::UNDO_SENTINEL,
}),
Event::Batch {
events,
description,
} => {
let inverted: Option<Vec<Event>> =
events.iter().rev().map(|e| e.inverse()).collect();
inverted.map(|inverted_events| Event::Batch {
events: inverted_events,
description: format!("Undo: {}", description),
})
}
Event::AddCursor {
cursor_id,
position,
anchor,
} => {
Some(Event::RemoveCursor {
cursor_id: *cursor_id,
position: *position,
anchor: *anchor,
})
}
Event::RemoveCursor {
cursor_id,
position,
anchor,
} => {
Some(Event::AddCursor {
cursor_id: *cursor_id,
position: *position,
anchor: *anchor,
})
}
Event::MoveCursor {
cursor_id,
old_position,
new_position,
old_anchor,
new_anchor,
old_sticky_column,
new_sticky_column,
} => {
Some(Event::MoveCursor {
cursor_id: *cursor_id,
old_position: *new_position,
new_position: *old_position,
old_anchor: *new_anchor,
new_anchor: *old_anchor,
old_sticky_column: *new_sticky_column,
new_sticky_column: *old_sticky_column,
})
}
Event::AddOverlay { .. } => {
None
}
Event::RemoveOverlay { .. } => {
None
}
Event::ClearNamespace { .. } => {
None
}
Event::Scroll { line_offset } => Some(Event::Scroll {
line_offset: -line_offset,
}),
Event::SetViewport { top_line: _ } => {
None
}
Event::ChangeMode { mode: _ } => {
None
}
_ => None,
}
}
pub fn modifies_buffer(&self) -> bool {
match self {
Event::Insert { .. } | Event::Delete { .. } => true,
Event::Batch { events, .. } => events.iter().any(|e| e.modifies_buffer()),
_ => false,
}
}
pub fn is_write_action(&self) -> bool {
match self {
Event::Insert { .. } | Event::Delete { .. } => true,
Event::AddCursor { .. } | Event::RemoveCursor { .. } => true,
Event::Batch { events, .. } => events.iter().any(|e| e.is_write_action()),
_ => false,
}
}
pub fn cursor_id(&self) -> Option<CursorId> {
match self {
Event::Insert { cursor_id, .. }
| Event::Delete { cursor_id, .. }
| Event::MoveCursor { cursor_id, .. }
| Event::AddCursor { cursor_id, .. }
| Event::RemoveCursor { cursor_id, .. } => Some(*cursor_id),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEntry {
pub event: Event,
pub timestamp: u64,
pub description: Option<String>,
}
impl LogEntry {
pub fn new(event: Event) -> Self {
Self {
event,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64,
description: None,
}
}
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
}
#[derive(Debug, Clone)]
pub struct Snapshot {
pub log_index: usize,
pub buffer_state: (),
pub cursor_positions: Vec<(CursorId, usize, Option<usize>)>,
}
pub struct EventLog {
entries: Vec<LogEntry>,
current_index: usize,
snapshots: Vec<Snapshot>,
snapshot_interval: usize,
stream_file: Option<std::fs::File>,
saved_at_index: Option<usize>,
}
impl EventLog {
pub fn new() -> Self {
Self {
entries: Vec::new(),
current_index: 0,
snapshots: Vec::new(),
snapshot_interval: 100,
stream_file: None,
saved_at_index: Some(0), }
}
pub fn mark_saved(&mut self) {
self.saved_at_index = Some(self.current_index);
}
pub fn is_at_saved_position(&self) -> bool {
match self.saved_at_index {
None => false,
Some(saved_idx) if saved_idx == self.current_index => true,
Some(saved_idx) => {
let (start, end) = if saved_idx < self.current_index {
(saved_idx, self.current_index)
} else {
(self.current_index, saved_idx)
};
self.entries[start..end]
.iter()
.all(|entry| !entry.event.modifies_buffer())
}
}
}
pub fn enable_streaming<P: AsRef<std::path::Path>>(&mut self, path: P) -> std::io::Result<()> {
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
writeln!(file, "# Event Log Stream")?;
writeln!(file, "# Started at: {}", chrono::Local::now())?;
writeln!(file, "# Format: JSON Lines (one event per line)")?;
writeln!(file, "#")?;
self.stream_file = Some(file);
Ok(())
}
pub fn disable_streaming(&mut self) {
self.stream_file = None;
}
pub fn log_render_state(
&mut self,
cursor_pos: usize,
screen_cursor_x: u16,
screen_cursor_y: u16,
buffer_len: usize,
) {
if let Some(ref mut file) = self.stream_file {
use std::io::Write;
let render_info = serde_json::json!({
"type": "render",
"timestamp": chrono::Local::now().to_rfc3339(),
"cursor_position": cursor_pos,
"screen_cursor": {"x": screen_cursor_x, "y": screen_cursor_y},
"buffer_length": buffer_len,
});
if let Err(e) = writeln!(file, "{render_info}") {
tracing::trace!("Warning: Failed to write render info to stream: {e}");
}
if let Err(e) = file.flush() {
tracing::trace!("Warning: Failed to flush event stream: {e}");
}
}
}
pub fn log_keystroke(&mut self, key_code: &str, modifiers: &str) {
if let Some(ref mut file) = self.stream_file {
use std::io::Write;
let keystroke_info = serde_json::json!({
"type": "keystroke",
"timestamp": chrono::Local::now().to_rfc3339(),
"key": key_code,
"modifiers": modifiers,
});
if let Err(e) = writeln!(file, "{keystroke_info}") {
tracing::trace!("Warning: Failed to write keystroke to stream: {e}");
}
if let Err(e) = file.flush() {
tracing::trace!("Warning: Failed to flush event stream: {e}");
}
}
}
pub fn append(&mut self, event: Event) -> usize {
if self.current_index < self.entries.len() {
self.entries.truncate(self.current_index);
}
if let Some(ref mut file) = self.stream_file {
use std::io::Write;
let stream_entry = serde_json::json!({
"index": self.entries.len(),
"timestamp": chrono::Local::now().to_rfc3339(),
"event": event,
});
if let Err(e) = writeln!(file, "{stream_entry}") {
tracing::trace!("Warning: Failed to write to event stream: {e}");
}
if let Err(e) = file.flush() {
tracing::trace!("Warning: Failed to flush event stream: {e}");
}
}
let entry = LogEntry::new(event);
self.entries.push(entry);
self.current_index = self.entries.len();
if self.entries.len() % self.snapshot_interval == 0 {
}
self.current_index - 1
}
pub fn current_index(&self) -> usize {
self.current_index
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn can_undo(&self) -> bool {
self.current_index > 0
}
pub fn can_redo(&self) -> bool {
self.current_index < self.entries.len()
}
pub fn undo(&mut self) -> Vec<Event> {
let mut inverse_events = Vec::new();
let mut found_write_action = false;
while self.can_undo() && !found_write_action {
self.current_index -= 1;
let event = &self.entries[self.current_index].event;
if event.is_write_action() {
found_write_action = true;
}
if let Some(inverse) = event.inverse() {
inverse_events.push(inverse);
}
}
inverse_events
}
pub fn redo(&mut self) -> Vec<Event> {
let mut events = Vec::new();
let mut found_write_action = false;
while self.can_redo() {
let event = self.entries[self.current_index].event.clone();
if found_write_action && event.is_write_action() {
break;
}
self.current_index += 1;
if event.is_write_action() {
found_write_action = true;
}
events.push(event);
}
events
}
pub fn entries(&self) -> &[LogEntry] {
&self.entries
}
pub fn range(&self, range: Range<usize>) -> &[LogEntry] {
&self.entries[range]
}
pub fn last_event(&self) -> Option<&Event> {
if self.current_index > 0 {
Some(&self.entries[self.current_index - 1].event)
} else {
None
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.current_index = 0;
self.snapshots.clear();
}
pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
use std::io::Write;
let file = std::fs::File::create(path)?;
let mut writer = std::io::BufWriter::new(file);
for entry in &self.entries {
let json = serde_json::to_string(entry)?;
writeln!(writer, "{json}")?;
}
Ok(())
}
pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
use std::io::BufRead;
let file = std::fs::File::open(path)?;
let reader = std::io::BufReader::new(file);
let mut log = Self::new();
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let entry: LogEntry = serde_json::from_str(&line)?;
log.entries.push(entry);
}
log.current_index = log.entries.len();
Ok(log)
}
pub fn set_snapshot_interval(&mut self, interval: usize) {
self.snapshot_interval = interval;
}
}
impl Default for EventLog {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
fn arb_event() -> impl Strategy<Value = Event> {
prop_oneof![
(0usize..1000, ".{1,50}").prop_map(|(pos, text)| Event::Insert {
position: pos,
text,
cursor_id: CursorId(0),
}),
(0usize..1000, 1usize..50).prop_map(|(pos, len)| Event::Delete {
range: pos..pos + len,
deleted_text: "x".repeat(len),
cursor_id: CursorId(0),
}),
]
}
proptest! {
#[test]
fn event_inverse_property(event in arb_event()) {
if let Some(inverse) = event.inverse() {
if let Some(double_inverse) = inverse.inverse() {
match (&event, &double_inverse) {
(Event::Insert { position: p1, text: t1, .. },
Event::Insert { position: p2, text: t2, .. }) => {
assert_eq!(p1, p2);
assert_eq!(t1, t2);
}
(Event::Delete { range: r1, deleted_text: dt1, .. },
Event::Delete { range: r2, deleted_text: dt2, .. }) => {
assert_eq!(r1, r2);
assert_eq!(dt1, dt2);
}
_ => {}
}
}
}
}
#[test]
fn undo_redo_inverse(events in prop::collection::vec(arb_event(), 1..20)) {
let mut log = EventLog::new();
for event in &events {
log.append(event.clone());
}
let after_append = log.current_index();
let mut undo_count = 0;
while log.can_undo() {
log.undo();
undo_count += 1;
}
assert_eq!(log.current_index(), 0);
assert_eq!(undo_count, events.len());
let mut redo_count = 0;
while log.can_redo() {
log.redo();
redo_count += 1;
}
assert_eq!(log.current_index(), after_append);
assert_eq!(redo_count, events.len());
}
#[test]
fn append_after_undo_truncates(
initial_events in prop::collection::vec(arb_event(), 2..10),
new_event in arb_event()
) {
let mut log = EventLog::new();
for event in &initial_events {
log.append(event.clone());
}
log.undo();
let index_after_undo = log.current_index();
log.append(new_event);
assert_eq!(log.current_index(), index_after_undo + 1);
assert!(!log.can_redo());
}
}
}
#[test]
fn test_event_log_append() {
let mut log = EventLog::new();
let event = Event::Insert {
position: 0,
text: "hello".to_string(),
cursor_id: CursorId(0),
};
let index = log.append(event);
assert_eq!(index, 0);
assert_eq!(log.current_index(), 1);
assert_eq!(log.entries().len(), 1);
}
#[test]
fn test_undo_redo() {
let mut log = EventLog::new();
log.append(Event::Insert {
position: 0,
text: "a".to_string(),
cursor_id: CursorId(0),
});
log.append(Event::Insert {
position: 1,
text: "b".to_string(),
cursor_id: CursorId(0),
});
assert_eq!(log.current_index(), 2);
assert!(log.can_undo());
assert!(!log.can_redo());
log.undo();
assert_eq!(log.current_index(), 1);
assert!(log.can_undo());
assert!(log.can_redo());
log.undo();
assert_eq!(log.current_index(), 0);
assert!(!log.can_undo());
assert!(log.can_redo());
log.redo();
assert_eq!(log.current_index(), 1);
}
#[test]
fn test_event_inverse() {
let insert = Event::Insert {
position: 5,
text: "hello".to_string(),
cursor_id: CursorId(0),
};
let inverse = insert.inverse().unwrap();
match inverse {
Event::Delete {
range,
deleted_text,
..
} => {
assert_eq!(range, 5..10);
assert_eq!(deleted_text, "hello");
}
_ => panic!("Expected Delete event"),
}
}
#[test]
fn test_truncate_on_new_event_after_undo() {
let mut log = EventLog::new();
log.append(Event::Insert {
position: 0,
text: "a".to_string(),
cursor_id: CursorId(0),
});
log.append(Event::Insert {
position: 1,
text: "b".to_string(),
cursor_id: CursorId(0),
});
log.undo();
assert_eq!(log.entries().len(), 2);
log.append(Event::Insert {
position: 1,
text: "c".to_string(),
cursor_id: CursorId(0),
});
assert_eq!(log.entries().len(), 2);
assert_eq!(log.current_index(), 2);
}
}