aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-13 15:25:49 -0500
committerCraig Jennings <c@cjennings.net>2026-06-13 15:25:49 -0500
commit20b251981bf01dbb25f494206de046023648252f (patch)
treeb77b177db595e4b22357cc14ad640c90ecde47a7
parent22c1e2816a9100e7fcb1e8a205efec727d68fcb9 (diff)
downloaddotemacs-20b251981bf01dbb25f494206de046023648252f.tar.gz
dotemacs-20b251981bf01dbb25f494206de046023648252f.zip
fix(help-config): non-destructive info open, clean cancel, drop dead config
Three audit defects in one file. cj/open-with-info-mode used cl-return-from inside a plain defun, so declining the save prompt threw "No catch for tag" instead of cancelling. The decision is now a pure cj/--info-open-plan and the command routes through it. A dead :hook (info-mode . info-persist-history-mode, which names a non-existent mode on the wrong hook) and an empty :preface are gone. The auto-mode-alist entry that mapped .info to that interactive, buffer-killing command is dropped, so find-file-noselect of a .info no longer destroys buffers. cj/open-with-info-mode stays an M-x command and C-h i still browses info files.
-rw-r--r--modules/help-config.el49
-rw-r--r--tests/test-help-config.el32
-rw-r--r--todo.org10
3 files changed, 68 insertions, 23 deletions
diff --git a/modules/help-config.el b/modules/help-config.el
index ce9fd861..df27cbea 100644
--- a/modules/help-config.el
+++ b/modules/help-config.el
@@ -50,24 +50,34 @@
;; ------------------------------------ Info -----------------------------------
- (defun cj/open-with-info-mode ()
- "Open the current buffer's file in Info mode if it's a valid info file.
+(defun cj/--info-open-plan (modified-p save-confirmed-p)
+ "Decide how to open a buffer in Info given its MODIFIED-P state.
+SAVE-CONFIRMED-P is the answer to the save prompt, meaningful only when
+MODIFIED-P. Returns `open', `save-then-open', or `cancel'."
+ (cond ((not modified-p) 'open)
+ (save-confirmed-p 'save-then-open)
+ (t 'cancel)))
+
+(defun cj/open-with-info-mode ()
+ "Open the current buffer's file in Info mode if it's a valid info file.
Preserves any unsaved changes and checks if the file exists."
- (interactive)
- (let ((file-name (buffer-file-name)))
- (when file-name
- (if (and (file-exists-p file-name)
- (string-match-p "\\.info\\'" file-name))
- (progn
- (when (buffer-modified-p)
- (if (y-or-n-p "Buffer has unsaved changes. Save before opening in Info? ")
- (save-buffer)
- (message "Operation canceled")
- (cl-return-from cj/open-with-info-mode)))
- (kill-buffer (current-buffer))
- (info file-name))
- (message "Not a valid info file: %s" file-name)))))
+ (interactive)
+ (let ((file-name (buffer-file-name)))
+ (when file-name
+ (if (and (file-exists-p file-name)
+ (string-match-p "\\.info\\'" file-name))
+ (let ((modified (buffer-modified-p)))
+ (pcase (cj/--info-open-plan
+ modified
+ (and modified
+ (y-or-n-p "Buffer has unsaved changes. Save before opening in Info? ")))
+ ('cancel (message "Operation canceled"))
+ (plan
+ (when (eq plan 'save-then-open) (save-buffer))
+ (kill-buffer (current-buffer))
+ (info file-name))))
+ (message "Not a valid info file: %s" file-name)))))
(defun cj/browse-info-files ()
"Browse and open .info or .info.gz files from user-emacs-directory."
@@ -96,7 +106,6 @@ Preserves any unsaved changes and checks if the file exists."
(:map Info-mode-map
("m" . bookmark-set) ;; Rebind 'm' from Info-menu to bookmark-set
("M" . Info-menu)) ;; Move Info-menu to 'M' instead
- :preface
:init
;; Add personal info files BEFORE Info mode initializes
;; (let ((personal-info-dir (expand-file-name "assets/info" user-emacs-directory)))
@@ -104,11 +113,7 @@ Preserves any unsaved changes and checks if the file exists."
;; (setq Info-directory-list (list personal-info-dir))))
;; the above makes the directory the info list. the below adds it to the default list
;; (add-to-list 'Info-default-directory-list personal-info-dir)))
- :hook
- (info-mode . info-persist-history-mode)
- :config
- ;; Make .info files open with our custom function
- (add-to-list 'auto-mode-alist '("\\.info\\'" . cj/open-with-info-mode)))
+ )
(provide 'help-config)
;;; help-config.el ends here.
diff --git a/tests/test-help-config.el b/tests/test-help-config.el
new file mode 100644
index 00000000..0ba95c41
--- /dev/null
+++ b/tests/test-help-config.el
@@ -0,0 +1,32 @@
+;;; test-help-config.el --- Tests for the Info-open decision logic -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; cj/open-with-info-mode opens the current .info buffer in Info, prompting to
+;; save first if the buffer is modified. The save/cancel/open decision is
+;; factored into the pure helper `cj/--info-open-plan' so it's testable without
+;; driving find-file, Info, or the save prompt. Declining the prompt must yield
+;; `cancel' -- the original cl-return-from inside a plain defun signalled
+;; "No catch for tag" instead of cancelling.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'help-config)
+
+(ert-deftest test-info-open-plan-unmodified-opens ()
+ "Normal: an unmodified buffer opens in Info directly."
+ (should (eq (cj/--info-open-plan nil nil) 'open)))
+
+(ert-deftest test-info-open-plan-modified-confirmed-saves-then-opens ()
+ "Normal: a modified buffer whose save is confirmed saves, then opens."
+ (should (eq (cj/--info-open-plan t t) 'save-then-open)))
+
+(ert-deftest test-info-open-plan-modified-declined-cancels ()
+ "Error/edge: a modified buffer whose save is declined cancels -- the path that
+used to signal \"No catch for tag\" via cl-return-from in a plain defun."
+ (should (eq (cj/--info-open-plan t nil) 'cancel)))
+
+(provide 'test-help-config)
+;;; test-help-config.el ends here
diff --git a/todo.org b/todo.org
index 20ab3811..8ce34d3e 100644
--- a/todo.org
+++ b/todo.org
@@ -948,11 +948,13 @@ Nothing requires =modules/ledger-config.el= (verified by grep), so .dat/.ledger/
** TODO [#B] eww quick-add bookmarks split the store and break the default file :bug:quick:solo:
=modules/eww-config.el:116-126= — quick-add let-binds =eww-bookmarks-directory= to ~/.emacs.d/eww-bookmarks/ (creating a DIRECTORY at the path where the daemon's default store expects a FILE ~/.emacs.d/eww-bookmarks). After one quick-add, B reads an unreadable path and quick-added bookmarks are invisible post-restart. Drop the let-binding or setq the directory once in :config so both commands share one store. From the 2026-06 config audit.
-** DOING [#B] help-config: three defects in one small file :bug:quick:solo:
+** DONE [#B] help-config: three defects in one small file :bug:quick:solo:
+CLOSED: [2026-06-13 Sat]
From the 2026-06 config audit, =modules/help-config.el=:
- =:67= — =cl-return-from= inside a plain =defun= (no cl-block): declining the save prompt signals "No catch for tag" instead of canceling. =cl-defun= or restructure.
- =:108= — =:hook (info-mode . info-persist-history-mode)= is dead twice: Info's hook is =Info-mode-hook= (capital I), and =info-persist-history-mode= doesn't exist anywhere. Implement the intent or delete.
- =:111= — auto-mode-alist maps .info to an interactive command that KILLS the buffer mid find-file — programmatic =find-file-noselect= of any .info destroys buffers and pops Info windows. Drop the entry; keep the explicit command. Zero test coverage on this module (the two broken paths are exactly the untested ones).
+Fixed 2026-06-13: (1) extracted the save/cancel/open decision into a pure =cj/--info-open-plan= and routed =cj/open-with-info-mode= through it — no more =cl-return-from=, declining cancels cleanly; (2) deleted the dead =:hook= and the empty =:preface=; (3) dropped the destructive =auto-mode-alist= .info entry (kept =cj/open-with-info-mode= as an M-x command and =cj/browse-info-files= on C-h i). New test-help-config.el covers the planner (open / save-then-open / cancel) — 3 green; module loads clean. Stale daemon state (the .info auto-mode entry + the bogus info-mode-hook entry) cleared by hand so the running session is correct without a restart. Interactive Info open + find-file-no-longer-destructive are a VERIFY.
** TODO [#B] modeline runs synchronous git on the redisplay path, unguarded :bug:solo:
=modules/modeline-config.el:173,154,145= — the mode-line :eval calls vc-backend/vc-state/vc-working-revision (synchronous git) on TTL expiry; a slow or unmounted filesystem stalls ALL redisplay. The cache key computes =file-truename= on every render (the "one stat per refresh" comment is wrong), and nothing is condition-case-wrapped, so a signal lands inside the mode-line eval. Defer the truename behind the TTL check; wrap the fetch in condition-case caching nil. From the 2026-06 config audit.
@@ -4350,6 +4352,12 @@ From the 2026-06-11 messenger-unification brainstorm. Google Voice has no offici
** TODO Manual testing and validation
Exercised once the phases above land.
+*** VERIFY info-mode open is non-destructive and cancels cleanly
+What we're verifying: opening a .info file no longer auto-kills the buffer, and the explicit cj/open-with-info-mode prompt cancels cleanly on decline. Fixed in modules/help-config.el; stale daemon state already cleared, so this also survives a fresh restart.
+- find-file a .info file (e.g. one under elpa) — it should open as an ordinary buffer, not vanish into Info
+- In that buffer, edit something, then M-x cj/open-with-info-mode; at the save prompt answer no
+- Repeat M-x cj/open-with-info-mode on an unmodified .info buffer
+Expected: find-file leaves the buffer intact (no auto-kill); declining the save prompt prints "Operation canceled" with no "No catch for tag" error; on an unmodified buffer it opens the file in Info.
*** VERIFY dwim-shell zip/backup/menu-key behave
What we're verifying: single-file zip makes a valid <name>.zip, the dated backup gets a real timestamp, and the dwim-shell menu is reachable on M-D in plain dired. Fixed in modules/dwim-shell-config.el, reloaded into the daemon.
- In dired, mark a single file, run the dwim-shell menu (M-D), pick Zip