diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-10 13:41:27 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-10 13:41:27 -0500 |
| commit | 09f3349af22c932a01f0788787cee9ab4f3c38a7 (patch) | |
| tree | d55cef5a67ff52051145177fd25f6a7faeafb096 | |
| parent | 3c840b0569ba3461cd61eabc32919f6899a25163 (diff) | |
| download | dotemacs-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.el | 53 | ||||
| -rw-r--r-- | tests/test-dirvish-config-ediff-pair.el | 71 |
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 |
