aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-10 13:27:51 -0500
committerCraig Jennings <c@cjennings.net>2026-05-10 13:27:51 -0500
commit2a9257006fcde777e217708b4593461a9e6c07a8 (patch)
tree1d4931fbe6132cc2a4e15b59787a58ef46f2fbc2
parentf2c45a7146568bea7eea12270688c8c8ed814c86 (diff)
downloaddotemacs-2a9257006fcde777e217708b4593461a9e6c07a8.tar.gz
dotemacs-2a9257006fcde777e217708b4593461a9e6c07a8.zip
refactor(dirvish): extract cj/--duplicate-file-name helper
The name-mangling logic in `cj/dirvish-duplicate-file' was inline -- inseparable from the dired side effects (existence check, copy, revert). Extract to `cj/--duplicate-file-name', a pure function from FILE to FILE-WITH-COPY-SUFFIX. Seven Normal/Boundary tests cover the cases I care about: typical extension, elisp file, no extension, multi-dot extensions (only the last dot counts), leading-dot dotfiles, relative paths, spaces in the base name. The wrapper retains the dired-mode interactive shape and now reads as a thin shell over the pure helper.
-rw-r--r--modules/dirvish-config.el44
-rw-r--r--tests/test-dirvish-config-duplicate-file-name.el59
2 files changed, 86 insertions, 17 deletions
diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el
index fadf09ca..fa5567ad 100644
--- a/modules/dirvish-config.el
+++ b/modules/dirvish-config.el
@@ -154,33 +154,43 @@ Filters for audio files, prompts for the playlist name, and saves the resulting
;;; ------------------------ Dirvish Duplicate File Copy ------------------------
+(defun cj/--duplicate-file-name (file)
+ "Return FILE's path with `-copy' inserted before the extension.
+
+Pure helper used by `cj/dirvish-duplicate-file'. Examples:
+ /tmp/report.pdf -> /tmp/report-copy.pdf
+ /home/foo/.bashrc -> /home/foo/.bashrc-copy
+ doc.txt -> doc-copy.txt
+ /tmp/archive.tar.gz -> /tmp/archive.tar-copy.gz"
+ (let* ((dir (file-name-directory file))
+ (base (file-name-base file))
+ (ext (or (file-name-extension file t) ""))
+ (new-name (concat base "-copy" ext)))
+ (if dir
+ (expand-file-name new-name dir)
+ new-name)))
+
(defun cj/dirvish-duplicate-file ()
- "Duplicate the file at point with '-copy' suffix before the extension.
+ "Duplicate the file at point with `-copy' suffix before the extension.
Examples:
report.pdf → report-copy.pdf
script.el → script-copy.el
README → README-copy"
(interactive)
- (let* ((file (dired-get-filename nil t))
- (dir (file-name-directory file))
- (base (file-name-base file))
- (ext (file-name-extension file t)) ; includes the dot
- (new-name (concat base "-copy" ext))
- (new-path (expand-file-name new-name dir)))
+ (let ((file (dired-get-filename nil t)))
(unless file
(user-error "No file at point"))
(when (file-directory-p file)
(user-error "Cannot duplicate directories, only files"))
-
- ;; Check if target already exists
- (when (file-exists-p new-path)
- (unless (y-or-n-p (format "File '%s' already exists. Overwrite? " new-name))
- (user-error "Cancelled")))
-
- ;; Copy the file
- (copy-file file new-path t)
- (revert-buffer)
- (message "Duplicated: %s → %s" (file-name-nondirectory file) new-name)))
+ (let* ((new-path (cj/--duplicate-file-name file))
+ (new-name (file-name-nondirectory new-path)))
+ (when (file-exists-p new-path)
+ (unless (y-or-n-p (format "File '%s' already exists. Overwrite? " new-name))
+ (user-error "Cancelled")))
+ (copy-file file new-path t)
+ (revert-buffer)
+ (message "Duplicated: %s → %s"
+ (file-name-nondirectory file) new-name))))
;;; ----------------------- Dirvish Open File Manager Here ----------------------
diff --git a/tests/test-dirvish-config-duplicate-file-name.el b/tests/test-dirvish-config-duplicate-file-name.el
new file mode 100644
index 00000000..8e69f28b
--- /dev/null
+++ b/tests/test-dirvish-config-duplicate-file-name.el
@@ -0,0 +1,59 @@
+;;; test-dirvish-config-duplicate-file-name.el --- Tests for the duplicate-file-name helper -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/--duplicate-file-name' is the pure name-mangling half of
+;; `cj/dirvish-duplicate-file': given an absolute file path, return the
+;; new path with `-copy' inserted before the extension. The interactive
+;; wrapper handles the dired side effects (existence check, copy, revert).
+
+;;; Code:
+
+(require 'ert)
+(require 'package)
+
+(setq package-user-dir (expand-file-name "elpa" user-emacs-directory))
+(package-initialize)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(add-to-list 'load-path (expand-file-name "elpa/dirvish-2.3.0/extensions"
+ user-emacs-directory))
+(require 'user-constants)
+(require 'keybindings)
+(require 'dirvish-config)
+
+(ert-deftest test-cj--duplicate-file-name-with-extension ()
+ "Normal: a file with an extension gets `-copy' before the dot."
+ (should (equal (cj/--duplicate-file-name "/tmp/report.pdf")
+ "/tmp/report-copy.pdf")))
+
+(ert-deftest test-cj--duplicate-file-name-elisp-extension ()
+ "Normal: works for elisp files (the project's daily case)."
+ (should (equal (cj/--duplicate-file-name "/home/foo/script.el")
+ "/home/foo/script-copy.el")))
+
+(ert-deftest test-cj--duplicate-file-name-no-extension ()
+ "Boundary: an extensionless file appends `-copy' at the end."
+ (should (equal (cj/--duplicate-file-name "/dir/README")
+ "/dir/README-copy")))
+
+(ert-deftest test-cj--duplicate-file-name-multiple-dots ()
+ "Boundary: only the last dot counts as the extension separator."
+ (should (equal (cj/--duplicate-file-name "/tmp/archive.tar.gz")
+ "/tmp/archive.tar-copy.gz")))
+
+(ert-deftest test-cj--duplicate-file-name-dotfile ()
+ "Boundary: a leading-dot file (no real extension) keeps the dot in the base."
+ (should (equal (cj/--duplicate-file-name "/home/foo/.bashrc")
+ "/home/foo/.bashrc-copy")))
+
+(ert-deftest test-cj--duplicate-file-name-relative-path ()
+ "Boundary: relative paths preserve their relative form."
+ (should (equal (cj/--duplicate-file-name "doc.txt")
+ "doc-copy.txt")))
+
+(ert-deftest test-cj--duplicate-file-name-spaces-in-name ()
+ "Boundary: spaces in the base name are preserved."
+ (should (equal (cj/--duplicate-file-name "/tmp/my file.txt")
+ "/tmp/my file-copy.txt")))
+
+(provide 'test-dirvish-config-duplicate-file-name)
+;;; test-dirvish-config-duplicate-file-name.el ends here