implemented undotree with redo
All checks were successful
Version Check / check-version (pull_request) Successful in 3s
All checks were successful
Version Check / check-version (pull_request) Successful in 3s
This commit is contained in:
37
src/app.rs
37
src/app.rs
@@ -1,5 +1,6 @@
|
|||||||
use crate::format::{ConfigItem, PathSegment};
|
use crate::format::{ConfigItem, PathSegment};
|
||||||
use tui_input::Input;
|
use tui_input::Input;
|
||||||
|
use crate::undo::UndoTree;
|
||||||
|
|
||||||
/// Represents the current operating mode of the application.
|
/// Represents the current operating mode of the application.
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
@@ -33,14 +34,15 @@ pub struct App {
|
|||||||
pub input: Input,
|
pub input: Input,
|
||||||
/// The current search query for filtering keys.
|
/// The current search query for filtering keys.
|
||||||
pub search_query: String,
|
pub search_query: String,
|
||||||
/// Stack of previous variable states for undo functionality.
|
/// Undo history structured as a tree
|
||||||
pub undo_stack: Vec<Vec<ConfigItem>>,
|
pub undo_tree: UndoTree,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
/// Initializes a new application instance with the provided variables.
|
/// Initializes a new application instance with the provided variables.
|
||||||
pub fn new(vars: Vec<ConfigItem>) -> Self {
|
pub fn new(vars: Vec<ConfigItem>) -> Self {
|
||||||
let initial_input = vars.first().and_then(|v| v.value.clone()).unwrap_or_default();
|
let initial_input = vars.first().and_then(|v| v.value.clone()).unwrap_or_default();
|
||||||
|
let undo_tree = UndoTree::new(vars.clone(), 0);
|
||||||
Self {
|
Self {
|
||||||
vars,
|
vars,
|
||||||
selected: 0,
|
selected: 0,
|
||||||
@@ -49,7 +51,7 @@ impl App {
|
|||||||
status_message: None,
|
status_message: None,
|
||||||
input: Input::new(initial_input),
|
input: Input::new(initial_input),
|
||||||
search_query: String::new(),
|
search_query: String::new(),
|
||||||
undo_stack: Vec::new(),
|
undo_tree,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,18 +327,16 @@ impl App {
|
|||||||
.unwrap_or(false)
|
.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) {
|
pub fn save_undo_state(&mut self) {
|
||||||
self.undo_stack.push(self.vars.clone());
|
self.undo_tree.push(self.vars.clone(), self.selected);
|
||||||
if self.undo_stack.len() > 50 {
|
|
||||||
self.undo_stack.remove(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reverts to the last saved state of variables.
|
/// Reverts to the previous state in the undo tree.
|
||||||
pub fn undo(&mut self) {
|
pub fn undo(&mut self) {
|
||||||
if let Some(previous_vars) = self.undo_stack.pop() {
|
if let Some(action) = self.undo_tree.undo() {
|
||||||
self.vars = previous_vars;
|
self.vars = action.state.clone();
|
||||||
|
self.selected = action.selected;
|
||||||
if self.selected >= self.vars.len() && !self.vars.is_empty() {
|
if self.selected >= self.vars.len() && !self.vars.is_empty() {
|
||||||
self.selected = self.vars.len() - 1;
|
self.selected = self.vars.len() - 1;
|
||||||
}
|
}
|
||||||
@@ -346,4 +346,19 @@ impl App {
|
|||||||
self.status_message = Some("Nothing to undo".to_string());
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -120,16 +120,17 @@ pub struct KeybindsConfig {
|
|||||||
pub prepend_item: String,
|
pub prepend_item: String,
|
||||||
pub delete_item: String,
|
pub delete_item: String,
|
||||||
pub undo: String,
|
pub undo: String,
|
||||||
}
|
pub redo: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for KeybindsConfig {
|
impl Default for KeybindsConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
down: "j".to_string(),
|
down: "j".to_string(),
|
||||||
up: "k".to_string(),
|
up: "k".to_string(),
|
||||||
edit: "i".to_string(),
|
edit: "i".to_string(),
|
||||||
edit_append: "A".to_string(),
|
edit_append: "a".to_string(),
|
||||||
edit_substitute: "S".to_string(),
|
edit_substitute: "s".to_string(),
|
||||||
save: ":w".to_string(),
|
save: ":w".to_string(),
|
||||||
quit: ":q".to_string(),
|
quit: ":q".to_string(),
|
||||||
normal_mode: "Esc".to_string(),
|
normal_mode: "Esc".to_string(),
|
||||||
@@ -142,9 +143,10 @@ impl Default for KeybindsConfig {
|
|||||||
prepend_item: "O".to_string(),
|
prepend_item: "O".to_string(),
|
||||||
delete_item: "dd".to_string(),
|
delete_item: "dd".to_string(),
|
||||||
undo: "u".to_string(),
|
undo: "u".to_string(),
|
||||||
|
redo: "U".to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Root configuration structure for mould.
|
/// Root configuration structure for mould.
|
||||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||||
|
|||||||
@@ -125,14 +125,13 @@ fn json_to_xml(value: &Value) -> String {
|
|||||||
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 4);
|
let mut writer = Writer::new_with_indent(Vec::new(), b' ', 4);
|
||||||
|
|
||||||
fn write_recursive(writer: &mut Writer<Vec<u8>>, value: &Value, key_name: Option<&str>) {
|
fn write_recursive(writer: &mut Writer<Vec<u8>>, value: &Value, key_name: Option<&str>) {
|
||||||
if let Some(k) = key_name {
|
if let Some(k) = key_name
|
||||||
if k == "$text" {
|
&& k == "$text" {
|
||||||
if let Some(s) = value.as_str() {
|
if let Some(s) = value.as_str() {
|
||||||
writer.write_event(Event::Text(BytesText::new(s))).unwrap();
|
writer.write_event(Event::Text(BytesText::new(s))).unwrap();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
match value {
|
match value {
|
||||||
Value::Object(map) => {
|
Value::Object(map) => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mod format;
|
|||||||
mod runner;
|
mod runner;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod resolver;
|
mod resolver;
|
||||||
|
mod undo;
|
||||||
|
|
||||||
use app::App;
|
use app::App;
|
||||||
use config::load_config;
|
use config::load_config;
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ where
|
|||||||
(&self.config.keybinds.prepend_item, "prepend_item"),
|
(&self.config.keybinds.prepend_item, "prepend_item"),
|
||||||
(&self.config.keybinds.delete_item, "delete_item"),
|
(&self.config.keybinds.delete_item, "delete_item"),
|
||||||
(&self.config.keybinds.undo, "undo"),
|
(&self.config.keybinds.undo, "undo"),
|
||||||
|
(&self.config.keybinds.redo, "redo"),
|
||||||
(&"a".to_string(), "add_missing"),
|
(&"a".to_string(), "add_missing"),
|
||||||
(&":".to_string(), "command"),
|
(&":".to_string(), "command"),
|
||||||
(&"q".to_string(), "quit"),
|
(&"q".to_string(), "quit"),
|
||||||
@@ -166,6 +167,7 @@ where
|
|||||||
"prepend_item" => self.app.add_array_item(false),
|
"prepend_item" => self.app.add_array_item(false),
|
||||||
"delete_item" => self.app.delete_selected(),
|
"delete_item" => self.app.delete_selected(),
|
||||||
"undo" => self.app.undo(),
|
"undo" => self.app.undo(),
|
||||||
|
"redo" => self.app.redo(),
|
||||||
"add_missing" => {
|
"add_missing" => {
|
||||||
self.app.save_undo_state();
|
self.app.save_undo_state();
|
||||||
self.add_missing_item();
|
self.add_missing_item();
|
||||||
|
|||||||
159
src/undo.rs
Normal file
159
src/undo.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
use crate::format::ConfigItem;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub struct EditAction {
|
||||||
|
pub state: Vec<ConfigItem>,
|
||||||
|
pub selected: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UndoNode {
|
||||||
|
pub action: EditAction,
|
||||||
|
pub parent: Option<usize>,
|
||||||
|
pub children: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UndoTree {
|
||||||
|
nodes: HashMap<usize, UndoNode>,
|
||||||
|
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<usize, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UndoTree {
|
||||||
|
pub fn new(initial_state: Vec<ConfigItem>, 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<ConfigItem>, 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user