diff --git a/src/app.rs b/src/app.rs index 5d1faa4..3b3b022 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use crate::format::{ConfigItem, PathSegment}; use tui_input::Input; +use crate::undo::UndoTree; /// Represents the current operating mode of the application. pub enum Mode { @@ -33,14 +34,15 @@ pub struct App { pub input: Input, /// The current search query for filtering keys. pub search_query: String, - /// Stack of previous variable states for undo functionality. - pub undo_stack: Vec>, + /// Undo history structured as a tree + pub undo_tree: UndoTree, } impl App { /// Initializes a new application instance with the provided variables. pub fn new(vars: Vec) -> Self { let initial_input = vars.first().and_then(|v| v.value.clone()).unwrap_or_default(); + let undo_tree = UndoTree::new(vars.clone(), 0); Self { vars, selected: 0, @@ -49,7 +51,7 @@ impl App { status_message: None, input: Input::new(initial_input), search_query: String::new(), - undo_stack: Vec::new(), + undo_tree, } } @@ -325,18 +327,16 @@ impl App { .unwrap_or(false) } - /// Saves the current state of variables to the undo stack. + /// Saves the current state of variables to the undo tree. pub fn save_undo_state(&mut self) { - self.undo_stack.push(self.vars.clone()); - if self.undo_stack.len() > 50 { - self.undo_stack.remove(0); - } + self.undo_tree.push(self.vars.clone(), self.selected); } - /// Reverts to the last saved state of variables. + /// Reverts to the previous state in the undo tree. pub fn undo(&mut self) { - if let Some(previous_vars) = self.undo_stack.pop() { - self.vars = previous_vars; + if let Some(action) = self.undo_tree.undo() { + self.vars = action.state.clone(); + self.selected = action.selected; if self.selected >= self.vars.len() && !self.vars.is_empty() { self.selected = self.vars.len() - 1; } @@ -346,4 +346,19 @@ impl App { self.status_message = Some("Nothing to undo".to_string()); } } + + /// Advances to the next state in the undo tree. + pub fn redo(&mut self) { + if let Some(action) = self.undo_tree.redo() { + self.vars = action.state.clone(); + self.selected = action.selected; + if self.selected >= self.vars.len() && !self.vars.is_empty() { + self.selected = self.vars.len() - 1; + } + self.sync_input_with_selected(); + self.status_message = Some("Redo applied".to_string()); + } else { + self.status_message = Some("Nothing to redo".to_string()); + } + } } \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 4eaf84e..dfe592a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -120,16 +120,17 @@ pub struct KeybindsConfig { pub prepend_item: String, pub delete_item: String, pub undo: String, -} + pub redo: String, + } -impl Default for KeybindsConfig { + impl Default for KeybindsConfig { fn default() -> Self { Self { down: "j".to_string(), up: "k".to_string(), edit: "i".to_string(), - edit_append: "A".to_string(), - edit_substitute: "S".to_string(), + edit_append: "a".to_string(), + edit_substitute: "s".to_string(), save: ":w".to_string(), quit: ":q".to_string(), normal_mode: "Esc".to_string(), @@ -142,9 +143,10 @@ impl Default for KeybindsConfig { prepend_item: "O".to_string(), delete_item: "dd".to_string(), undo: "u".to_string(), + redo: "U".to_string(), } } -} + } /// Root configuration structure for mould. #[derive(Debug, Deserialize, Serialize, Default, Clone)] diff --git a/src/format/hierarchical.rs b/src/format/hierarchical.rs index 51db009..686799f 100644 --- a/src/format/hierarchical.rs +++ b/src/format/hierarchical.rs @@ -125,14 +125,13 @@ fn json_to_xml(value: &Value) -> String { let mut writer = Writer::new_with_indent(Vec::new(), b' ', 4); fn write_recursive(writer: &mut Writer>, value: &Value, key_name: Option<&str>) { - if let Some(k) = key_name { - if k == "$text" { + if let Some(k) = key_name + && k == "$text" { if let Some(s) = value.as_str() { writer.write_event(Event::Text(BytesText::new(s))).unwrap(); } return; } - } match value { Value::Object(map) => { diff --git a/src/main.rs b/src/main.rs index d01abd6..c8476a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod format; mod runner; mod ui; mod resolver; +mod undo; use app::App; use config::load_config; diff --git a/src/runner.rs b/src/runner.rs index 47b5d95..3992ed6 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -128,6 +128,7 @@ where (&self.config.keybinds.prepend_item, "prepend_item"), (&self.config.keybinds.delete_item, "delete_item"), (&self.config.keybinds.undo, "undo"), + (&self.config.keybinds.redo, "redo"), (&"a".to_string(), "add_missing"), (&":".to_string(), "command"), (&"q".to_string(), "quit"), @@ -166,6 +167,7 @@ where "prepend_item" => self.app.add_array_item(false), "delete_item" => self.app.delete_selected(), "undo" => self.app.undo(), + "redo" => self.app.redo(), "add_missing" => { self.app.save_undo_state(); self.add_missing_item(); diff --git a/src/undo.rs b/src/undo.rs new file mode 100644 index 0000000..45c944d --- /dev/null +++ b/src/undo.rs @@ -0,0 +1,159 @@ +use crate::format::ConfigItem; +use std::collections::HashMap; + +pub struct EditAction { + pub state: Vec, + pub selected: usize, +} + +pub struct UndoNode { + pub action: EditAction, + pub parent: Option, + pub children: Vec, +} + +pub struct UndoTree { + nodes: HashMap, + current_node: usize, + next_id: usize, + // Track the latest child added to a node to know which branch to follow on redo + latest_branch: HashMap, +} + +impl UndoTree { + pub fn new(initial_state: Vec, initial_selected: usize) -> Self { + let root_id = 0; + let root_node = UndoNode { + action: EditAction { + state: initial_state, + selected: initial_selected, + }, + parent: None, + children: Vec::new(), + }; + + let mut nodes = HashMap::new(); + nodes.insert(root_id, root_node); + + Self { + nodes, + current_node: root_id, + next_id: 1, + latest_branch: HashMap::new(), + } + } + + pub fn push(&mut self, state: Vec, selected: usize) { + let new_id = self.next_id; + self.next_id += 1; + + let new_node = UndoNode { + action: EditAction { state, selected }, + parent: Some(self.current_node), + children: Vec::new(), + }; + + // Add to nodes + self.nodes.insert(new_id, new_node); + + // Update parent's children + if let Some(parent_node) = self.nodes.get_mut(&self.current_node) { + parent_node.children.push(new_id); + } + + // Record this as the latest branch for the parent + self.latest_branch.insert(self.current_node, new_id); + + // Move current pointer + self.current_node = new_id; + } + + pub fn undo(&mut self) -> Option<&EditAction> { + if let Some(current) = self.nodes.get(&self.current_node) + && let Some(parent_id) = current.parent { + self.current_node = parent_id; + return self.nodes.get(&parent_id).map(|n| &n.action); + } + None + } + + pub fn redo(&mut self) -> Option<&EditAction> { + if let Some(next_id) = self.latest_branch.get(&self.current_node).copied() { + self.current_node = next_id; + return self.nodes.get(&next_id).map(|n| &n.action); + } else { + // Fallback: if there is no recorded latest branch but there are children + if let Some(current) = self.nodes.get(&self.current_node) + && let Some(&first_child_id) = current.children.last() { + self.current_node = first_child_id; + self.latest_branch.insert(self.current_node, first_child_id); + return self.nodes.get(&first_child_id).map(|n| &n.action); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::format::{ItemStatus, ValueType}; + + fn dummy_item(key: &str) -> ConfigItem { + ConfigItem { + key: key.to_string(), + path: vec![], + value: Some(key.to_string()), + template_value: None, + default_value: None, + depth: 0, + is_group: false, + status: ItemStatus::Present, + value_type: ValueType::String, + } + } + + #[test] + fn test_undo_redo_tree() { + let state1 = vec![dummy_item("A")]; + let mut tree = UndoTree::new(state1.clone(), 0); + + // Push state 2 + let state2 = vec![dummy_item("B")]; + tree.push(state2.clone(), 1); + + // Push state 3 + let state3 = vec![dummy_item("C")]; + tree.push(state3.clone(), 2); + + // Undo -> State 2 + let action = tree.undo().unwrap(); + assert_eq!(action.state[0].key, "B"); + assert_eq!(action.selected, 1); + + // Undo -> State 1 + let action = tree.undo().unwrap(); + assert_eq!(action.state[0].key, "A"); + assert_eq!(action.selected, 0); + + // Undo again -> None (already at root) + assert!(tree.undo().is_none()); + + // Redo -> State 2 + let action = tree.redo().unwrap(); + assert_eq!(action.state[0].key, "B"); + assert_eq!(action.selected, 1); + + // Branching: Push State 4 (from State 2) + let state4 = vec![dummy_item("D")]; + tree.push(state4.clone(), 3); + + // Undo -> State 2 + let action = tree.undo().unwrap(); + assert_eq!(action.state[0].key, "B"); + + // Redo -> State 4 (follows latest branch D, not old branch C) + let action = tree.redo().unwrap(); + assert_eq!(action.state[0].key, "D"); + } +}