Skip to content

Commit

Permalink
Support undo/redo
Browse files Browse the repository at this point in the history
  • Loading branch information
ImJeremyHe committed Jan 24, 2025
1 parent daf5a3a commit e4bb432
Show file tree
Hide file tree
Showing 14 changed files with 296 additions and 124 deletions.
1 change: 1 addition & 0 deletions crates/controller/src/api/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ fn new_workbook() {
idx: None,
})],
undoable: true,
init: false,
}));

match result.status {
Expand Down
5 changes: 4 additions & 1 deletion crates/controller/src/api/workbook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ impl Workbook {

/// Create a workbook from a .xlsx file.
pub fn from_file(buf: &[u8], book_name: String) -> Result<Self> {
let controller = Controller::from_file(book_name, buf)?;
let mut controller = Controller::from_file(book_name, buf)?;
controller
.version_manager
.set_init_status(controller.status.clone());
Ok(Workbook {
controller,
cell_positioners: new_locked(HashMap::new()),
Expand Down
6 changes: 5 additions & 1 deletion crates/controller/src/controller/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ impl<'a> Executor<'a> {

let result = result.calc()?;

if payload_action.undoable {
if payload_action.init {
result
.version_manager
.set_init_status(result.status.clone());
} else if payload_action.undoable {
result.version_manager.record(
result.status.clone(),
payload_action,
Expand Down
11 changes: 7 additions & 4 deletions crates/controller/src/controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ impl Default for Controller {
version_manager: VersionManager::default(),
async_func_manager: AsyncFuncManager::default(),
};
let add_sheet = PayloadsAction::new(false).add_payload(CreateSheet {
idx: 0,
new_name: String::from("Sheet1"),
});
let add_sheet = PayloadsAction::new()
.add_payload(CreateSheet {
idx: 0,
new_name: String::from("Sheet1"),
})
.set_init(true);
empty.handle_action(EditAction::Payloads(add_sheet));
empty
}
Expand Down Expand Up @@ -282,6 +284,7 @@ mod tests {
content: String::from("=ABS(1)"),
})],
undoable: true,
init: false,
};
wb.handle_action(EditAction::Payloads(payloads_action));
let len = wb.status.formula_manager.formulas.len();
Expand Down
19 changes: 17 additions & 2 deletions crates/controller/src/edit_action/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,31 @@ impl From<PayloadsAction> for EditAction {
pub struct PayloadsAction {
pub payloads: Vec<EditPayload>,
pub undoable: bool,
// An action that is used to customize the initial status of a new workbook.
// This action is `undoable` but its new status should be recorded to hiistory.
pub init: bool,
}

impl PayloadsAction {
pub fn new(undoable: bool) -> Self {
pub fn new() -> Self {
PayloadsAction {
payloads: vec![],
undoable,
undoable: false,
init: false,
}
}

pub fn set_undoable(mut self, v: bool) -> Self {
self.undoable = v;
self
}

pub fn set_init(mut self, v: bool) -> Self {
self.init = v;
self.undoable = false;
self
}

pub fn add_payload<P: Payload>(mut self, payload: P) -> Self {
self.payloads.push(payload.into());
self
Expand Down
139 changes: 139 additions & 0 deletions crates/controller/src/version_manager/manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use logisheets_base::SheetId;
use std::collections::{HashMap, VecDeque};

#[derive(Debug, Default)]
pub struct VersionManagerHelper<S: Clone, D, const SIZE: usize> {
pub(crate) version: u32,
pub(crate) undo_stack: VecDeque<S>,
pub(crate) redo_stack: VecDeque<S>,
pub(crate) diff_undo_stack: VecDeque<HashMap<SheetId, D>>,
pub(crate) diff_redo_stack: VecDeque<HashMap<SheetId, D>>,
pub(crate) current_status: S,
pub(crate) current_diffs: HashMap<SheetId, D>,
}

impl<S: Clone, D, const SIZE: usize> VersionManagerHelper<S, D, SIZE> {
pub fn version(&self) -> u32 {
self.version
}

pub fn set_init_status(&mut self, current: S) {
self.undo_stack.clear();
self.redo_stack.clear();
self.diff_undo_stack.clear();
self.diff_redo_stack.clear();
self.current_diffs.clear();
self.current_status = current;
self.current_diffs = HashMap::new();
}

pub fn add_status(&mut self, current: S, sheet_diff: HashMap<SheetId, D>) {
self.redo_stack.clear();
self.diff_redo_stack.clear();

if self.undo_stack.len() >= SIZE {
self.undo_stack.pop_front();
self.diff_undo_stack.pop_front();
}

let mut current = current;
let mut sheet_diff = sheet_diff;

std::mem::swap(&mut current, &mut self.current_status);
std::mem::swap(&mut sheet_diff, &mut self.current_diffs);

self.undo_stack.push_back(current);
self.diff_undo_stack.push_back(sheet_diff);
}

pub fn undo(&mut self) -> Option<S> {
let mut status = self.undo_stack.pop_back()?;
let mut payloads = self.diff_undo_stack.pop_back()?;

std::mem::swap(&mut status, &mut self.current_status);
std::mem::swap(&mut payloads, &mut self.current_diffs);

self.redo_stack.push_back(status.clone());
self.diff_redo_stack.push_back(payloads);

Some(self.current_status.clone())
}

pub fn redo(&mut self) -> Option<S> {
let mut status = self.redo_stack.pop_back()?;
let mut payloads = self.diff_redo_stack.pop_back()?;

std::mem::swap(&mut status, &mut self.current_status);
std::mem::swap(&mut payloads, &mut self.current_diffs);

self.undo_stack.push_back(status.clone());
self.diff_undo_stack.push_back(payloads);

Some(self.current_status.clone())
}
}

#[cfg(test)]
mod tests {
use std::collections::HashMap;

use super::VersionManagerHelper;

pub type VersionManagerHelperTest = VersionManagerHelper<u16, (), 3>;

#[test]
fn test_undo_redo() {
let mut manager = VersionManagerHelperTest::default();
manager.add_status(1, HashMap::default());
manager.add_status(2, HashMap::default());
manager.undo();
assert_eq!(manager.current_status, 1);
manager.redo();
assert_eq!(manager.current_status, 2);
// Nothing to redo, should remain current status
manager.redo();
assert_eq!(manager.current_status, 2);
}

#[test]
fn test_set_init_status() {
let mut manager = VersionManagerHelperTest::default();
manager.set_init_status(4);
manager.undo();
assert_eq!(manager.current_status, 4);
manager.redo();
assert_eq!(manager.current_status, 4);
manager.add_status(12, HashMap::default());
manager.undo();
assert_eq!(manager.current_status, 4);
manager.redo();
assert_eq!(manager.current_status, 12);
manager.undo();
manager.undo();
manager.undo();
manager.undo();
manager.undo();
assert_eq!(manager.current_status, 4);
manager.redo();
assert_eq!(manager.current_status, 12);
}

#[test]
fn test_exceed_limit_history_size() {
let mut manager = VersionManagerHelperTest::default();
manager.add_status(1, HashMap::default());
manager.add_status(2, HashMap::default());
manager.add_status(3, HashMap::default());
manager.add_status(4, HashMap::default());
manager.add_status(5, HashMap::default());
manager.undo();
manager.undo();
manager.undo();
manager.undo();
manager.undo();
manager.undo();
manager.undo();
manager.undo();
assert_eq!(manager.current_status, 2);
}
}
65 changes: 4 additions & 61 deletions crates/controller/src/version_manager/mod.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
pub mod ctx;
pub mod diff;
mod manager;

use std::collections::{HashMap, HashSet, VecDeque};
use std::collections::HashSet;

use logisheets_base::{CellId, SheetId};
use manager::VersionManagerHelper;

use crate::{controller::status::Status, edit_action::PayloadsAction};

use self::diff::{convert_payloads_to_sheet_diff, Diff, SheetDiff};

const HISTORY_SIZE: usize = 50;

/// VersionManager records the history of a workbook and the payloads. It can help
/// users find out the minimal updates at a certain version.
#[derive(Debug, Default)]
pub struct VersionManager {
version: u32,
undo_stack: VecDeque<Status>,
redo_stack: VecDeque<Status>,
diff_undo_stack: VecDeque<HashMap<SheetId, SheetDiff>>,
diff_redo_stack: VecDeque<HashMap<SheetId, SheetDiff>>,
current_status: Status,
current_diffs: HashMap<SheetId, SheetDiff>,
}
pub type VersionManager = VersionManagerHelper<Status, SheetDiff, HISTORY_SIZE>;

impl VersionManager {
pub fn version(&self) -> u32 {
self.version
}

pub fn record(
&mut self,
mut status: Status,
Expand All @@ -40,51 +28,6 @@ impl VersionManager {
self.version += 1;
}

fn add_status(&mut self, current: Status, sheet_diff: HashMap<SheetId, SheetDiff>) {
self.redo_stack.clear();
self.diff_redo_stack.clear();

if self.undo_stack.len() >= HISTORY_SIZE {
self.undo_stack.pop_front();
self.diff_undo_stack.pop_front();
}

let mut current = current;
let mut sheet_diff = sheet_diff;

std::mem::swap(&mut current, &mut self.current_status);
std::mem::swap(&mut sheet_diff, &mut self.current_diffs);

self.undo_stack.push_back(current);
self.diff_undo_stack.push_back(sheet_diff);
}

pub fn undo(&mut self) -> Option<Status> {
let mut status = self.undo_stack.pop_back()?;
let mut payloads = self.diff_undo_stack.pop_back()?;

std::mem::swap(&mut status, &mut self.current_status);
std::mem::swap(&mut payloads, &mut self.current_diffs);

self.redo_stack.push_back(status.clone());
self.diff_redo_stack.push_back(payloads);

Some(status)
}

pub fn redo(&mut self) -> Option<Status> {
let mut status = self.redo_stack.pop_back()?;
let mut payloads = self.diff_redo_stack.pop_back()?;

std::mem::swap(&mut status, &mut self.current_status);
std::mem::swap(&mut payloads, &mut self.current_diffs);

self.undo_stack.push_back(status.clone());
self.diff_undo_stack.push_back(payloads);

Some(status)
}

// `None` means that users can not update the workbook to the latest one incremently.
pub fn get_sheet_diffs_from_version(&self, sheet: SheetId, version: u32) -> Option<SheetDiff> {
if version > self.version {
Expand Down
Loading

0 comments on commit e4bb432

Please sign in to comment.