summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-10 13:41:27 -0500
committerCraig Jennings <c@cjennings.net>2026-05-10 13:41:27 -0500
commit09f3349af22c932a01f0788787cee9ab4f3c38a7 (patch)
treed55cef5a67ff52051145177fd25f6a7faeafb096
parent3c840b0569ba3461cd61eabc32919f6899a25163 (diff)
downloaddotemacs-09f3349af22c932a01f0788787cee9ab4f3c38a7.tar.gz
dotemacs-09f3349af22c932a01f0788787cee9ab4f3c38a7.zip
refactor(dirvish): extract cj/--ediff-pair-from-files; fix 0-files crash
`cj/dired-ediff-files' had its pair-determination logic inline: count check, prompt fallback when only one file was marked, and the older-first ordering for `ediff-files'. Lift it into `cj/--ediff-pair-from-files' -- pure given the file list, an injected prompt thunk, and a newer-than-p comparator -- so tests stay independent of mtimes and the dired prompt. While extracting, surface a latent bug: with zero marked files the original code fell through to `(file-newer-than-file-p nil nil)' and crashed with a wrong-type-argument error. Replace the crash with a clear `user-error' ("No files marked"), and add a regression test. The 3+ files case keeps its existing user-error message. Five Normal/Boundary/Error tests cover both ordering directions, the one-file prompt path, and both error counts.
-rw-r--r--modules/dirvish-config.el53
-rw-r--r--tests/test-dirvish-config-ediff-pair.el71
2 files changed, 107 insertions, 17 deletions
diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el
index 438d21fa..86ecfd2d 100644
--- a/modules/dirvish-config.el
+++ b/modules/dirvish-config.el
@@ -32,26 +32,45 @@
;;; ----------------------------- Dired Ediff Files -----------------------------
+(defun cj/--ediff-pair-from-files (files prompt-fn newer-than-p)
+ "Return a (OLDER . NEWER) cons for ediff'ing FILES.
+
+FILES is the list of marked file paths. PROMPT-FN is a thunk used to
+acquire a second file when only one is marked. NEWER-THAN-P is a binary
+predicate (a b) -> non-nil when A is newer than B.
+
+Signals `user-error' for zero or 3+ files; the latter matches the original
+contract, the former replaces a latent crash where the caller fell through
+to (file-newer-than-file-p nil ...).
+
+Pure helper used by `cj/dired-ediff-files'."
+ (let ((n (length files)))
+ (cond
+ ((zerop n)
+ (user-error "No files marked"))
+ ((> n 2)
+ (user-error "No more than 2 files should be marked"))
+ (t
+ (let ((file1 (car files))
+ (file2 (or (cadr files) (funcall prompt-fn))))
+ (if (funcall newer-than-p file1 file2)
+ (cons file2 file1)
+ (cons file1 file2)))))))
+
(defun cj/dired-ediff-files ()
"Ediff two selected files within Dired."
(interactive)
- (let ((files (dired-get-marked-files))
- (wnd (current-window-configuration)))
- (if (<= (length files) 2)
- (let ((file1 (car files))
- (file2 (if (cdr files)
- (cadr files)
- (read-file-name
- "file: "
- (dired-dwim-target-directory)))))
- (if (file-newer-than-file-p file1 file2)
- (ediff-files file2 file1)
- (ediff-files file1 file2))
- (add-hook 'ediff-after-quit-hook-internal
- (lambda ()
- (setq ediff-after-quit-hook-internal nil)
- (set-window-configuration wnd))))
- (error "No more than 2 files should be marked"))))
+ (let* ((wnd (current-window-configuration))
+ (pair (cj/--ediff-pair-from-files
+ (dired-get-marked-files)
+ (lambda ()
+ (read-file-name "file: " (dired-dwim-target-directory)))
+ #'file-newer-than-file-p)))
+ (ediff-files (car pair) (cdr pair))
+ (add-hook 'ediff-after-quit-hook-internal
+ (lambda ()
+ (setq ediff-after-quit-hook-internal nil)
+ (set-window-configuration wnd)))))
;; ------------------------ Create Playlist From Marked ------------------------
diff --git a/tests/test-dirvish-config-ediff-pair.el b/tests/test-dirvish-config-ediff-pair.el
new file mode 100644
index 00000000..fc9e5e70
--- /dev/null
+++ b/tests/test-dirvish-config-ediff-pair.el
@@ -0,0 +1,71 @@
+;;; test-dirvish-config-ediff-pair.el --- Tests for the ediff-pair helper -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/--ediff-pair-from-files' picks the (FILE1 . FILE2) pair that
+;; `cj/dired-ediff-files' should hand to `ediff-files'. The pair is
+;; (older . newer) so ediff renders the older revision on the left.
+;; The helper takes a prompt callback (used when only one file is
+;; marked) and a "newer than" comparator, so tests stay independent of
+;; real mtimes and the dired prompt.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(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--ediff-pair-two-files-first-is-newer ()
+ "Normal: file1 newer than file2 -> pair is (file2 . file1) so older runs first."
+ (let ((newer-fn (lambda (a _b) (string= a "/a"))))
+ (should (equal (cj/--ediff-pair-from-files
+ '("/a" "/b") (lambda () "/unused") newer-fn)
+ '("/b" . "/a")))))
+
+(ert-deftest test-cj--ediff-pair-two-files-second-is-newer ()
+ "Normal: file1 older than file2 -> pair preserves the input order."
+ (let ((newer-fn (lambda (_a _b) nil)))
+ (should (equal (cj/--ediff-pair-from-files
+ '("/a" "/b") (lambda () "/unused") newer-fn)
+ '("/a" . "/b")))))
+
+(ert-deftest test-cj--ediff-pair-one-file-calls-prompt ()
+ "Boundary: a single marked file triggers the prompt callback for the second."
+ (let ((prompt-calls 0)
+ (newer-fn (lambda (_a _b) nil)))
+ (let ((pair (cj/--ediff-pair-from-files
+ '("/only")
+ (lambda () (cl-incf prompt-calls) "/picked")
+ newer-fn)))
+ (should (= prompt-calls 1))
+ (should (equal pair '("/only" . "/picked"))))))
+
+(ert-deftest test-cj--ediff-pair-three-files-signals-user-error ()
+ "Error: more than two files signals user-error without prompting."
+ (let ((prompt-calls 0))
+ (should-error
+ (cj/--ediff-pair-from-files
+ '("/a" "/b" "/c")
+ (lambda () (cl-incf prompt-calls) "/x")
+ (lambda (_a _b) nil))
+ :type 'user-error)
+ (should (zerop prompt-calls))))
+
+(ert-deftest test-cj--ediff-pair-zero-files-signals-user-error ()
+ "Error: zero files signals user-error -- regression for the latent crash
+where `cj/dired-ediff-files' fell through to `(file-newer-than-file-p nil ...)'."
+ (should-error
+ (cj/--ediff-pair-from-files
+ '() (lambda () "/x") (lambda (_a _b) nil))
+ :type 'user-error))
+
+(provide 'test-dirvish-config-ediff-pair)
+;;; test-dirvish-config-ediff-pair.el ends here