diff --git a/yazi-core/src/manager/commands/bulk_rename.rs b/yazi-core/src/manager/commands/bulk_rename.rs index 88bf79d2c..17bda7571 100644 --- a/yazi-core/src/manager/commands/bulk_rename.rs +++ b/yazi-core/src/manager/commands/bulk_rename.rs @@ -53,7 +53,7 @@ impl Manager { return Ok(()); } - let todo: Vec<_> = old.into_iter().zip(new).filter(|(o, n)| o != n).collect(); + let todo = Self::prioritized_paths(old, new); if todo.is_empty() { return Ok(()); } @@ -117,4 +117,90 @@ impl Manager { stdin().read_exact(&mut [0]).await?; Ok(()) } + + fn prioritized_paths(old: Vec, new: Vec) -> Vec<(PathBuf, PathBuf)> { + let orders: HashMap<_, _> = old.iter().enumerate().map(|(i, p)| (p, i)).collect(); + let mut incomes: HashMap<_, _> = old.iter().map(|p| (p, false)).collect(); + let mut todos: HashMap<_, _> = old + .iter() + .zip(new) + .map(|(o, n)| { + incomes.get_mut(&n).map(|b| *b = true); + (o, n) + }) + .collect(); + + let mut sorted = Vec::with_capacity(old.len()); + while !todos.is_empty() { + // Paths that are non-incomes and don't need to be prioritized in this round + let mut outcomes: Vec<_> = incomes.iter().filter(|(_, &b)| !b).map(|(&p, _)| p).collect(); + outcomes.sort_unstable_by(|a, b| orders[b].cmp(&orders[a])); + + // If there're no outcomes, it means there are cycles in the renaming + if outcomes.is_empty() { + let mut remain: Vec<_> = todos.into_iter().map(|(o, n)| (o.clone(), n)).collect(); + remain.sort_unstable_by(|(a, _), (b, _)| orders[a].cmp(&orders[b])); + sorted.reverse(); + sorted.extend(remain); + return sorted; + } + + for old in outcomes { + let Some(new) = todos.remove(old) else { unreachable!() }; + incomes.remove(&old); + incomes.get_mut(&new).map(|b| *b = false); + sorted.push((old.clone(), new)); + } + } + sorted.reverse(); + sorted + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sort() { + fn cmp(input: &[(&str, &str)], expected: &[(&str, &str)]) { + let sorted = Manager::prioritized_paths( + input.iter().map(|&(o, _)| o.into()).collect(), + input.iter().map(|&(_, n)| n.into()).collect(), + ); + let sorted: Vec<_> = + sorted.iter().map(|(o, n)| (o.to_str().unwrap(), n.to_str().unwrap())).collect(); + assert_eq!(sorted, expected); + } + + #[rustfmt::skip] + cmp( + &[("2", "3"), ("1", "2"), ("3", "4")], + &[("3", "4"), ("2", "3"), ("1", "2")] + ); + + #[rustfmt::skip] + cmp( + &[("1", "3"), ("2", "3"), ("3", "4")], + &[("3", "4"), ("1", "3"), ("2", "3")] + ); + + #[rustfmt::skip] + cmp( + &[("2", "1"), ("1", "2")], + &[("2", "1"), ("1", "2")] + ); + + #[rustfmt::skip] + cmp( + &[("3", "2"), ("2", "1"), ("1", "3"), ("a", "b"), ("b", "c")], + &[("b", "c"), ("a", "b"), ("3", "2"), ("2", "1"), ("1", "3")] + ); + + #[rustfmt::skip] + cmp( + &[("b", "b_"), ("a", "a_"), ("c", "c_")], + &[("b", "b_"), ("a", "a_"), ("c", "c_")], + ); + } }