release/0.5.0 #15
145
src/app.rs
145
src/app.rs
@@ -163,7 +163,6 @@ impl App {
|
|||||||
pub fn enter_insert(&mut self, variant: InsertVariant) {
|
pub fn enter_insert(&mut self, variant: InsertVariant) {
|
||||||
if let Some(var) = self.vars.get(self.selected)
|
if let Some(var) = self.vars.get(self.selected)
|
||||||
&& !var.is_group {
|
&& !var.is_group {
|
||||||
self.save_undo_state();
|
|
||||||
self.mode = Mode::Insert;
|
self.mode = Mode::Insert;
|
||||||
match variant {
|
match variant {
|
||||||
InsertVariant::Start => {
|
InsertVariant::Start => {
|
||||||
@@ -184,6 +183,7 @@ impl App {
|
|||||||
/// Commits the current input and transitions the application into Normal Mode.
|
/// Commits the current input and transitions the application into Normal Mode.
|
||||||
pub fn enter_normal(&mut self) {
|
pub fn enter_normal(&mut self) {
|
||||||
self.commit_input();
|
self.commit_input();
|
||||||
|
self.save_undo_state();
|
||||||
self.mode = Mode::Normal;
|
self.mode = Mode::Normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +193,6 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save_undo_state();
|
|
||||||
let selected_path = self.vars[self.selected].path.clone();
|
let selected_path = self.vars[self.selected].path.clone();
|
||||||
let is_group = self.vars[self.selected].is_group;
|
let is_group = self.vars[self.selected].is_group;
|
||||||
|
|
||||||
@@ -245,67 +244,129 @@ impl App {
|
|||||||
self.selected = self.vars.len() - 1;
|
self.selected = self.vars.len() - 1;
|
||||||
}
|
}
|
||||||
self.sync_input_with_selected();
|
self.sync_input_with_selected();
|
||||||
|
self.save_undo_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a new item to an array if the selected item is part of one.
|
/// Adds a new item relative to the selected item.
|
||||||
pub fn add_array_item(&mut self, after: bool) {
|
pub fn add_item(&mut self, after: bool) {
|
||||||
if self.vars.is_empty() {
|
if self.vars.is_empty() {
|
||||||
|
let new_key = "NEW_VAR".to_string();
|
||||||
|
self.vars.push(ConfigItem {
|
||||||
|
key: new_key.clone(),
|
||||||
|
path: vec![PathSegment::Key(new_key)],
|
||||||
|
value: Some("".to_string()),
|
||||||
|
template_value: None,
|
||||||
|
default_value: None,
|
||||||
|
depth: 0,
|
||||||
|
is_group: false,
|
||||||
|
status: crate::format::ItemStatus::Modified,
|
||||||
|
value_type: crate::format::ValueType::String,
|
||||||
|
});
|
||||||
|
self.selected = 0;
|
||||||
|
self.sync_input_with_selected();
|
||||||
|
self.save_undo_state();
|
||||||
|
self.enter_insert(InsertVariant::Start);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save_undo_state();
|
let selected_item = self.vars[self.selected].clone();
|
||||||
let (base_path, idx, depth) = {
|
|
||||||
let selected_item = &self.vars[self.selected];
|
// 1. Determine new item properties (path, key, depth, position)
|
||||||
if selected_item.is_group {
|
let mut new_path;
|
||||||
return;
|
let new_depth;
|
||||||
}
|
let insert_pos;
|
||||||
let path = &selected_item.path;
|
let mut is_array_item = false;
|
||||||
|
|
||||||
|
if let Some(PathSegment::Index(idx)) = selected_item.path.last() {
|
||||||
|
// ARRAY ITEM LOGIC
|
||||||
|
is_array_item = true;
|
||||||
|
let base_path = selected_item.path[..selected_item.path.len() - 1].to_vec();
|
||||||
|
let new_idx = if after { idx + 1 } else { *idx };
|
||||||
|
insert_pos = if after { self.selected + 1 } else { self.selected };
|
||||||
|
|
||||||
if let Some(PathSegment::Index(idx)) = path.last() {
|
// Shift subsequent indices
|
||||||
(path[..path.len() - 1].to_vec(), *idx, selected_item.depth)
|
for var in self.vars.iter_mut() {
|
||||||
} else {
|
if var.path.starts_with(&base_path) && var.path.len() > base_path.len()
|
||||||
return;
|
&& let PathSegment::Index(i) = var.path[base_path.len()]
|
||||||
}
|
&& i >= new_idx {
|
||||||
};
|
var.path[base_path.len()] = PathSegment::Index(i + 1);
|
||||||
|
if var.path.len() == base_path.len() + 1 {
|
||||||
let new_idx = if after { idx + 1 } else { idx };
|
var.key = format!("[{}]", i + 1);
|
||||||
let insert_pos = if after {
|
}
|
||||||
self.selected + 1
|
|
||||||
} else {
|
|
||||||
self.selected
|
|
||||||
};
|
|
||||||
|
|
||||||
// 1. Shift all items in this array that have index >= new_idx
|
|
||||||
for var in self.vars.iter_mut() {
|
|
||||||
if var.path.starts_with(&base_path) && var.path.len() > base_path.len()
|
|
||||||
&& let PathSegment::Index(i) = var.path[base_path.len()]
|
|
||||||
&& i >= new_idx {
|
|
||||||
var.path[base_path.len()] = PathSegment::Index(i + 1);
|
|
||||||
if var.path.len() == base_path.len() + 1 {
|
|
||||||
var.key = format!("[{}]", i + 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new_path = base_path;
|
||||||
|
new_path.push(PathSegment::Index(new_idx));
|
||||||
|
new_depth = selected_item.depth;
|
||||||
|
} else if after && selected_item.is_group {
|
||||||
|
// ADD AS CHILD OF GROUP
|
||||||
|
insert_pos = self.selected + 1;
|
||||||
|
new_path = selected_item.path.clone();
|
||||||
|
new_depth = selected_item.depth + 1;
|
||||||
|
} else {
|
||||||
|
// ADD AS SIBLING
|
||||||
|
let parent_path = if selected_item.path.len() > 1 {
|
||||||
|
selected_item.path[..selected_item.path.len() - 1].to_vec()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
insert_pos = if after {
|
||||||
|
let mut p = self.selected + 1;
|
||||||
|
while p < self.vars.len() && self.vars[p].path.starts_with(&selected_item.path) {
|
||||||
|
p += 1;
|
||||||
|
}
|
||||||
|
p
|
||||||
|
} else {
|
||||||
|
self.selected
|
||||||
|
};
|
||||||
|
|
||||||
|
new_path = parent_path;
|
||||||
|
new_depth = selected_item.depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Insert new item
|
// 2. Generate a unique key for non-array items
|
||||||
let mut new_path = base_path;
|
let final_key = if is_array_item {
|
||||||
new_path.push(PathSegment::Index(new_idx));
|
if let Some(PathSegment::Index(idx)) = new_path.last() {
|
||||||
|
format!("[{}]", idx)
|
||||||
|
} else {
|
||||||
|
"NEW_VAR".to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut count = 1;
|
||||||
|
let mut candidate = "NEW_VAR".to_string();
|
||||||
|
let parent_path_slice = new_path.as_slice();
|
||||||
|
|
||||||
|
while self.vars.iter().any(|v| {
|
||||||
|
v.path.starts_with(parent_path_slice)
|
||||||
|
&& v.path.len() == parent_path_slice.len() + 1
|
||||||
|
&& v.key == candidate
|
||||||
|
}) {
|
||||||
|
candidate = format!("NEW_VAR_{}", count);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
new_path.push(PathSegment::Key(candidate.clone()));
|
||||||
|
candidate
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Insert new item
|
||||||
let new_item = ConfigItem {
|
let new_item = ConfigItem {
|
||||||
key: format!("[{}]", new_idx),
|
key: final_key,
|
||||||
path: new_path,
|
path: new_path,
|
||||||
value: Some("".to_string()),
|
value: Some("".to_string()),
|
||||||
template_value: None,
|
template_value: None,
|
||||||
default_value: None,
|
default_value: None,
|
||||||
depth,
|
depth: new_depth,
|
||||||
is_group: false,
|
is_group: false,
|
||||||
status: crate::format::ItemStatus::Modified,
|
status: crate::format::ItemStatus::Modified,
|
||||||
value_type: crate::format::ValueType::String,
|
value_type: crate::format::ValueType::String,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.vars.insert(insert_pos, new_item);
|
self.vars.insert(insert_pos, new_item);
|
||||||
self.selected = insert_pos;
|
self.selected = insert_pos;
|
||||||
self.sync_input_with_selected();
|
self.sync_input_with_selected();
|
||||||
|
self.save_undo_state();
|
||||||
self.enter_insert(InsertVariant::Start);
|
self.enter_insert(InsertVariant::Start);
|
||||||
self.status_message = None;
|
self.status_message = None;
|
||||||
}
|
}
|
||||||
@@ -361,4 +422,4 @@ impl App {
|
|||||||
self.status_message = Some("Nothing to redo".to_string());
|
self.status_message = Some("Nothing to redo".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,13 +163,12 @@ where
|
|||||||
"previous_match" => self.app.jump_previous_match(),
|
"previous_match" => self.app.jump_previous_match(),
|
||||||
"jump_top" => self.app.jump_top(),
|
"jump_top" => self.app.jump_top(),
|
||||||
"jump_bottom" => self.app.jump_bottom(),
|
"jump_bottom" => self.app.jump_bottom(),
|
||||||
"append_item" => self.app.add_array_item(true),
|
"append_item" => self.app.add_item(true),
|
||||||
"prepend_item" => self.app.add_array_item(false),
|
"prepend_item" => self.app.add_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(),
|
"redo" => self.app.redo(),
|
||||||
"add_missing" => {
|
"add_missing" => {
|
||||||
self.app.save_undo_state();
|
|
||||||
self.add_missing_item();
|
self.add_missing_item();
|
||||||
}
|
}
|
||||||
"command" => {
|
"command" => {
|
||||||
@@ -207,6 +206,7 @@ where
|
|||||||
var.value = var.template_value.clone();
|
var.value = var.template_value.clone();
|
||||||
}
|
}
|
||||||
self.app.sync_input_with_selected();
|
self.app.sync_input_with_selected();
|
||||||
|
self.app.save_undo_state();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
40
src/undo.rs
40
src/undo.rs
@@ -83,10 +83,11 @@ impl UndoTree {
|
|||||||
return self.nodes.get(&next_id).map(|n| &n.action);
|
return self.nodes.get(&next_id).map(|n| &n.action);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: if there is no recorded latest branch but there are children
|
// Fallback: if there is no recorded latest branch but there are children
|
||||||
if let Some(current) = self.nodes.get(&self.current_node)
|
let current_id = self.current_node;
|
||||||
|
if let Some(current) = self.nodes.get(¤t_id)
|
||||||
&& let Some(&first_child_id) = current.children.last() {
|
&& let Some(&first_child_id) = current.children.last() {
|
||||||
self.current_node = first_child_id;
|
self.current_node = first_child_id;
|
||||||
self.latest_branch.insert(self.current_node, first_child_id);
|
self.latest_branch.insert(current_id, first_child_id);
|
||||||
return self.nodes.get(&first_child_id).map(|n| &n.action);
|
return self.nodes.get(&first_child_id).map(|n| &n.action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,16 +145,43 @@ mod tests {
|
|||||||
assert_eq!(action.state[0].key, "B");
|
assert_eq!(action.state[0].key, "B");
|
||||||
assert_eq!(action.selected, 1);
|
assert_eq!(action.selected, 1);
|
||||||
|
|
||||||
// Branching: Push State 4 (from State 2)
|
// Redo -> State 3
|
||||||
|
let action = tree.redo().unwrap();
|
||||||
|
assert_eq!(action.state[0].key, "C");
|
||||||
|
assert_eq!(action.selected, 2);
|
||||||
|
|
||||||
|
// Branching: Undo twice to State 1
|
||||||
|
tree.undo();
|
||||||
|
tree.undo();
|
||||||
|
|
||||||
|
// Push State 4 (from State 1)
|
||||||
let state4 = vec![dummy_item("D")];
|
let state4 = vec![dummy_item("D")];
|
||||||
tree.push(state4.clone(), 3);
|
tree.push(state4.clone(), 3);
|
||||||
|
|
||||||
// Undo -> State 2
|
// Undo -> State 1
|
||||||
let action = tree.undo().unwrap();
|
let action = tree.undo().unwrap();
|
||||||
assert_eq!(action.state[0].key, "B");
|
assert_eq!(action.state[0].key, "A");
|
||||||
|
|
||||||
// Redo -> State 4 (follows latest branch D, not old branch C)
|
// Redo -> State 4 (follows latest branch D, not old branch B)
|
||||||
let action = tree.redo().unwrap();
|
let action = tree.redo().unwrap();
|
||||||
assert_eq!(action.state[0].key, "D");
|
assert_eq!(action.state[0].key, "D");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_redo_fallback_fix() {
|
||||||
|
let state1 = vec![dummy_item("A")];
|
||||||
|
let mut tree = UndoTree::new(state1.clone(), 0);
|
||||||
|
|
||||||
|
let state2 = vec![dummy_item("B")];
|
||||||
|
tree.push(state2.clone(), 1);
|
||||||
|
|
||||||
|
tree.undo();
|
||||||
|
// Redo should move to state 2
|
||||||
|
let action = tree.redo().unwrap();
|
||||||
|
assert_eq!(action.state[0].key, "B");
|
||||||
|
|
||||||
|
// Calling redo again should NOT change the current node or returned action
|
||||||
|
// (since it's already at the latest child)
|
||||||
|
assert!(tree.redo().is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user