From 7695d23984d2dcc807584b0659b62a0a75dacf85 Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Wed, 18 Mar 2026 16:28:15 -0500 Subject: [PATCH] undo/redo --- src/action.rs | 47 +++++++++++++++++++++++++++++++++++++- src/app/mod.rs | 4 +++- src/config.rs | 6 +++++ src/edit_action.rs | 56 +++++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 2 -- 5 files changed, 108 insertions(+), 7 deletions(-) diff --git a/src/action.rs b/src/action.rs index f39636d..6afca38 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,4 +1,4 @@ -use std::{cmp::min, mem::swap}; +use std::{cmp::min, mem::{replace, swap}}; use crate::{BYTES_PER_LINE, app::{App, Mode, PartialAction}, edit_action::EditAction}; @@ -51,6 +51,9 @@ pub enum Action { ExtendLineAbove, Delete, + + Undo, + Redo, } impl App { @@ -103,6 +106,9 @@ impl App { Action::ExtendLineAbove => self.extend_line_above(), Action::Delete => self.delete(), + + Action::Undo => self.undo(), + Action::Redo => self.redo(), } } @@ -351,6 +357,45 @@ impl App { self.mode = Mode::Normal; } } + + fn undo(&mut self) { + if self.time_traveling == Some(0) { return } + + let current_date = self.time_traveling + .map_or(self.edit_history.len() - 1, |date| date - 1); + + self.time_traveling = Some(current_date); + + let edit_action = replace( + &mut self.edit_history[current_date], + EditAction::Placeholder + ); + + self.undo_edit(&edit_action); + + self.edit_history[current_date] = edit_action; + } + + fn redo(&mut self) { + let Some(previous_date) = self.time_traveling else { return }; + + let current_date = previous_date + 1; + + self.time_traveling = if current_date == self.edit_history.len() { + None + } else { + Some(current_date) + }; + + let edit_action = replace( + &mut self.edit_history[previous_date], + EditAction::Placeholder + ); + + self.execute_edit(&edit_action); + + self.edit_history[previous_date] = edit_action; + } } // helpers diff --git a/src/app/mod.rs b/src/app/mod.rs index 1b61f28..26216fb 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -23,7 +23,8 @@ pub struct App { pub partial_replace: Option, pub edit_history: Vec, - // some index to keep track of where we are? edit_prophecy? + // the index *after* the latest edit action + pub time_traveling: Option, pub logs: Vec, } @@ -102,6 +103,7 @@ impl App { partial_replace: None, edit_history: Vec::new(), + time_traveling: None, logs: Vec::new(), } diff --git a/src/config.rs b/src/config.rs index 40ff008..b56f08b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -133,6 +133,9 @@ impl Default for Config { ("X".try_into().unwrap(), Action::ExtendLineAbove), ("d".try_into().unwrap(), Action::Delete), + + ("u".try_into().unwrap(), Action::Undo), + ("U".try_into().unwrap(), Action::Redo), ].into()), (Some(PartialAction::Goto), [ ("j".try_into().unwrap(), Action::GotoLineStart), @@ -175,6 +178,9 @@ impl Default for Config { ("X".try_into().unwrap(), Action::ExtendLineAbove), ("d".try_into().unwrap(), Action::Delete), + + ("u".try_into().unwrap(), Action::Undo), + ("U".try_into().unwrap(), Action::Redo), ].into()) ].into()) ].into() diff --git a/src/edit_action.rs b/src/edit_action.rs index e741eff..b5dc9fb 100644 --- a/src/edit_action.rs +++ b/src/edit_action.rs @@ -3,6 +3,12 @@ use crate::{app::App, cursor::Cursor}; #[derive(Debug)] pub enum EditAction { + // this is a hacky workaround to allow disjoint borrows for + // undoing/redoing edits. because the undo/redo operations need + // to borrow an EditAction from edit_history, but should never touch + // edit_history themselves, we can swap out the action in question, + // then swap it back in once the undo/redo is done + Placeholder, Delete { cursor: Cursor, old_data: Vec @@ -11,17 +17,31 @@ pub enum EditAction { cursor: Cursor, old_data: Vec, new_byte: u8 - } + }, + // Insert { + // cursor: Cursor, + // which side of cursor? append/insert + // new_data: Vec + // } } impl App { pub fn execute_and_add(&mut self, edit_action: EditAction) { + assert!(!matches!(edit_action, EditAction::Placeholder)); + self.execute_edit(&edit_action); + + if let Some(date) = self.time_traveling { + self.edit_history.truncate(date); + self.time_traveling = None; + } + self.edit_history.push(edit_action); } - fn execute_edit(&mut self, edit_action: &EditAction) { + pub fn execute_edit(&mut self, edit_action: &EditAction) { match edit_action { + EditAction::Placeholder => unreachable!(), EditAction::Delete { cursor, .. } => self.delete_at(*cursor), EditAction::Replace { cursor, old_data: _, new_byte @@ -29,6 +49,16 @@ impl App { } } + pub fn undo_edit(&mut self, edit_action: &EditAction) { + match edit_action { + EditAction::Placeholder => unreachable!(), + EditAction::Delete { cursor, old_data } => self.undo_delete_at(*cursor, old_data), + EditAction::Replace { + cursor, old_data, .. + } => self.undo_replace_at_with(*cursor, old_data), + } + } + fn delete_at(&mut self, cursor: Cursor) { self.contents.drain(cursor.range()); @@ -36,8 +66,28 @@ impl App { self.cursor.collapse(); } + fn undo_delete_at(&mut self, cursor: Cursor, old_data: &[u8]) { + let cursor_start = min(cursor.head, cursor.tail); + + self.contents.splice( + cursor_start..cursor_start, + old_data.iter().copied() + ); + + self.cursor = cursor; + } + fn replace_at_with(&mut self, cursor: Cursor, new_byte: u8) { - self.contents[self.cursor.range()].fill(new_byte); + self.contents[cursor.range()].fill(new_byte); + + self.cursor = cursor; + } + + fn undo_replace_at_with(&mut self, cursor: Cursor, old_data: &[u8]) { + self.contents.splice( + cursor.range(), + old_data.iter().copied() + ); self.cursor = cursor; } diff --git a/src/main.rs b/src/main.rs index 77da572..96ee97b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,8 +19,6 @@ const CHUNKS_PER_LINE: usize = BYTES_PER_LINE / BYTES_PER_CHUNK; // TODO: // - undo/redo // - modifications -// - replace -// - partial action(s) // - insert/append // - mode // - how this works with edit history is strange :/