Repository: flxzt/rnote (language: rust)

Issue:
Duplicate only allows one "-" in the file name
**Describe the bug**  
If a file has a hyphen in the name e.g. `test - test.rnote` and I select to duplicate it, the new file removes the existing hyphen and adds one at the end with the new number e.g. `testtest - 1.rnote`.

**To Reproduce**  
Steps to reproduce the behavior:
1. Create a file with a hyphen e.g. `Exam 1 - Task 1.rnote`
2. Duplicate the file for the second task (right click on the three dots, duplicate)
3. Check the name of the new file, mine is named `Exam 1Task 1 - 1.rnote` removing the first hyphen.


**Expected behavior**  
I would expect that the duplicate only adds an `- 1.rnote` to the existing file, without "changing" there the name.

**Console Output**  
Run the app in the terminal with `flatpak --env=RUST_LOG=rnote=debug --env=RUST_BACKTRACE=1 run com.github.flxzt.rnote`
and post the output.
```sh
❯ flatpak --env=RUST_LOG=rnote=debug --env=RUST_BACKTRACE=1 run com.github.flxzt.rnote
2024-08-20T07:52:40.050378Z DEBUG rnote: .. tracing subscriber initialized.
```
I think this log does not really help in this issue, or I did not found the proper one.

**Screenshots**  
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**  
 - OS: Fedora 40 - Workstation Edition
 - App Version: Rnote v0.11.0
 - Installation Source: Flatpak
 - Desktop Environment: Gnome 46
 - Display Server: Wayland
 - Input Source: Mouse and Keyboard

**Additional context**  
I was able to reproduce the described bug also on a Windows 10 machine, also running Rnote v0.11.0.



Discussion / hints:

Fix patch:
diff --git a/crates/rnote-ui/src/workspacebrowser/filerow/actions/duplicate.rs b/crates/rnote-ui/src/workspacebrowser/filerow/actions/duplicate.rs
index 055ff0ea50..37b2e42bda 100644
--- a/crates/rnote-ui/src/workspacebrowser/filerow/actions/duplicate.rs
+++ b/crates/rnote-ui/src/workspacebrowser/filerow/actions/duplicate.rs
@@ -12,18 +12,23 @@ use tracing::debug;
 
 /// The regex used to search for duplicated files
 /// ```text
-/// - Look for delimiter
+/// - Look for original filename lazily
 /// |         - Look for `<delim>1`/`<delim>2`/`<delim>123`/...
-/// |         |        - Look for the rest after the `<delim><num>` part
+/// |         |        - Look for the file extension after the `<delim><num>` part
 /// |         |       |       - At the end of the file name
 /// |         |       |       |
-/// |        \d*      |       $
+/// |         (?<delim>{}\d+) $
 /// |                 |
-/// DELIM       (?P<rest>(.*))
+/// (?<filename>.*?)  (?<extension>\.\w+)
 /// ```
 static DUP_REGEX: Lazy<Regex> = Lazy::new(|| {
-    Regex::new(&(crate::utils::FILE_DUP_SUFFIX_DELIM_REGEX.to_string() + r"\d*(?P<rest>(.*))$"))
-        .unwrap()
+    // Incorporate FILE_DUP_SUFFIX_DELIM_REGEX dynamically into the regex pattern
+    let regex_pattern: String = format!(
+        r"^(?<filename>.*?)(?<delim>{}\d*)?(?<extension>\.\w+)?$",
+        crate::utils::FILE_DUP_SUFFIX_DELIM_REGEX
+    );
+
+    Regex::new(&regex_pattern).unwrap()
 });
 
 /// Create a new `duplicate` action.
@@ -99,10 +104,14 @@ fn generate_destination_path(source: impl AsRef<Path>) -> anyhow::Result<PathBuf
             "file of source path '{adjusted_source_path:?}' does not have a file stem."
         ));
     };
+
+    let source_extension = adjusted_source_path.extension();
+
+    // Loop to find the next available duplicate filename
     loop {
         destination_path.set_file_name(generate_duplicate_filename(
             source_stem,
-            adjusted_source_path.extension(),
+            source_extension,
             duplicate_index,
         ));
 
@@ -118,7 +127,7 @@ fn generate_destination_path(source: impl AsRef<Path>) -> anyhow::Result<PathBuf
 /// Creates the duplicate-filename by the given information about the source.
 ///
 /// For example:
-/// "test.txt" => "test-1.txt" => "test-2.txt"
+/// "test.txt" => "test - 1.txt" => "test - 2.txt"
 fn generate_duplicate_filename(
     source_stem: &OsStr,
     source_extension: Option<&OsStr>,
@@ -136,15 +145,13 @@ fn generate_duplicate_filename(
 
 /// Recursively removes found file-name suffixes that match with the above regex from the given file path.
 fn remove_dup_suffix(source: impl AsRef<Path>) -> PathBuf {
-    let mut removed = source.as_ref().to_string_lossy().to_string();
-    loop {
-        let new = DUP_REGEX.replace(&removed, "$rest").to_string();
-        if removed == new {
-            break;
-        }
-        removed = new;
-    }
-    PathBuf::from(removed)
+    let original = source.as_ref().to_string_lossy().to_string();
+    // Preserve the base and extension, remove duplicate suffix
+    let new = DUP_REGEX
+        .replace(&original, "$filename$extension")
+        .to_string();
+
+    PathBuf::from(new)
 }
 
 #[cfg(test)]
@@ -189,5 +196,17 @@ mod tests {
             let expected = PathBuf::from("normal_folder");
             assert_eq!(expected, remove_dup_suffix(&source));
         }
+
+        {
+            let source = PathBuf::from("normal - file.rnote");
+            let expected = source.clone();
+            assert_eq!(expected, remove_dup_suffix(&source));
+        }
+
+        {
+            let source = PathBuf::from("normal - file - 1.rnote");
+            let expected = PathBuf::from("normal - file.rnote");
+            assert_eq!(expected, remove_dup_suffix(&source));
+        }
     }
 }


Test patch: