release/0.5.0 #15

Merged
nvrl merged 16 commits from release/0.5.0 into main 2026-03-18 22:50:11 +01:00
3 changed files with 140 additions and 51 deletions
Showing only changes of commit 51625ed296 - Show all commits

View File

@@ -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,37 +244,47 @@ impl App {
self.selected = self.vars.len() - 1; self.selected = self.vars.len() - 1;
} }
self.sync_input_with_selected(); self.sync_input_with_selected();
}
/// Adds a new item to an array if the selected item is part of one.
pub fn add_array_item(&mut self, after: bool) {
if self.vars.is_empty() {
return;
}
self.save_undo_state(); self.save_undo_state();
let (base_path, idx, depth) = { }
let selected_item = &self.vars[self.selected];
if selected_item.is_group { /// Adds a new item relative to the selected item.
pub fn add_item(&mut self, after: bool) {
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;
} }
let path = &selected_item.path;
if let Some(PathSegment::Index(idx)) = path.last() { let selected_item = self.vars[self.selected].clone();
(path[..path.len() - 1].to_vec(), *idx, selected_item.depth)
} else {
return;
}
};
let new_idx = if after { idx + 1 } else { idx }; // 1. Determine new item properties (path, key, depth, position)
let insert_pos = if after { let mut new_path;
self.selected + 1 let new_depth;
} else { let insert_pos;
self.selected let mut is_array_item = false;
};
// 1. Shift all items in this array that have index >= new_idx 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 };
// Shift subsequent indices
for var in self.vars.iter_mut() { for var in self.vars.iter_mut() {
if var.path.starts_with(&base_path) && var.path.len() > base_path.len() if var.path.starts_with(&base_path) && var.path.len() > base_path.len()
&& let PathSegment::Index(i) = var.path[base_path.len()] && let PathSegment::Index(i) = var.path[base_path.len()]
@@ -287,17 +296,68 @@ impl App {
} }
} }
// 2. Insert new item new_path = base_path;
let mut new_path = base_path;
new_path.push(PathSegment::Index(new_idx)); 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. Generate a unique key for non-array items
let final_key = if is_array_item {
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,
@@ -306,6 +366,7 @@ impl App {
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;
} }

View File

@@ -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();
} }
} }

View File

@@ -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(&current_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());
}
} }