diff --git a/crates/project/src/comp.rs b/crates/project/src/comp.rs index 5fc6b38a0..c0c401c82 100644 --- a/crates/project/src/comp.rs +++ b/crates/project/src/comp.rs @@ -271,7 +271,7 @@ impl Comp { if !self.layers.iter().any(|l| l.name == base) { return base.to_string(); } - let stem = base.trim_end_matches(|c: char| c.is_ascii_digit()).trim_end(); + let stem = crate::item::name_stem(base); // The suffixes in use, gathered in one pass: trying each candidate against every layer // made importing many layers of one name cubic. let taken: std::collections::HashSet = self diff --git a/crates/project/src/item.rs b/crates/project/src/item.rs index 4fcc06ab9..b055c60ba 100644 --- a/crates/project/src/item.rs +++ b/crates/project/src/item.rs @@ -8,12 +8,18 @@ use super::{Arc, BTreeMap, Deserialize, FrameRate, Label, Project, Serialize, Ti /// another layer of the same name. A number set off by a space is a counter and is replaced /// (`Red Solid 1` → `Red Solid 2`, `shot 2` → `shot 3`); digits that end a word belong to the /// name and stay (`v-b1.mp4` → `v-b1.mp4 2`, `clip2.mov` → `clip2.mov 2`). -pub(crate) fn numbered_name(base: &str, taken: impl Fn(&str) -> bool) -> String { +/// `base` without a trailing ` N`: digits are dropped only when a space separates them from a +/// nonempty stem, so `v-b1.mp4` and `Layer1` keep theirs. +pub(crate) fn name_stem(base: &str) -> &str { let head = base.trim_end_matches(|c: char| c.is_ascii_digit()); - let stem = match head.strip_suffix(' ') { + match head.strip_suffix(' ') { Some(stem) if head.len() < base.len() && !stem.trim_end().is_empty() => stem.trim_end(), _ => base.trim_end(), - }; + } +} + +pub(crate) fn numbered_name(base: &str, taken: impl Fn(&str) -> bool) -> String { + let stem = name_stem(base); (2u64..).map(|k| format!("{stem} {k}")).find(|n| !taken(n)).unwrap_or_default() } diff --git a/crates/project/src/tests.rs b/crates/project/src/tests.rs index 89c01c42e..80ba3c771 100644 --- a/crates/project/src/tests.rs +++ b/crates/project/src/tests.rs @@ -154,6 +154,10 @@ fn unique_names_and_cycles() { d.layers.push(l); } assert_eq!(d.unique_layer_name("Red Solid 1"), "Red Solid 2"); + // Digits that end a word stay in the stem. + let mut e = c.clone(); + e.layers[0].name = "v-b1.mp4".into(); + assert_eq!(e.unique_layer_name("v-b1.mp4"), "v-b1.mp4 2"); assert!(p.comp_contains(cid, cid)); }