From d797b7ad5d6af70d7d1ab082f824df07cf5bd536 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Fri, 15 May 2026 00:54:01 -0500 Subject: feat(custom-buffer-file): extend buffer-source dispatch to mu4e and Info Add two dispatchers to cj/buffer-source-functions so C-; b p yields a useful link form in two more major modes. mu4e-view-mode returns "mu4e:msgid:" so the result pastes into org as a clickable link and matches mu4e's own org-protocol handler. Falls through to buffer-file-name when point isn't on a real message. Info-mode returns "info:(manual)node" -- the form org-info-store-link produces. file-name-base only strips one extension, so a compressed "emacs.info.gz" comes back as "emacs.info"; trim the trailing ".info" to get the bare manual name. Falls through when Info hasn't populated its current-file / current-node vars yet. Tests cover normal + boundary fallthrough for each new mode. --- modules/custom-buffer-file.el | 23 ++++++++- .../test-custom-buffer-file-copy-buffer-source.el | 54 ++++++++++++++++++++++ todo.org | 4 +- 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/modules/custom-buffer-file.el b/modules/custom-buffer-file.el index 8c6014d3..d8ce6bee 100644 --- a/modules/custom-buffer-file.el +++ b/modules/custom-buffer-file.el @@ -218,7 +218,28 @@ When called interactively, prompts for confirmation if target file exists." '((eww-mode . (lambda () (eww-current-url))) (elfeed-show-mode . (lambda () (elfeed-entry-link elfeed-show-entry))) (dired-mode . (lambda () (dired-get-filename nil t))) - (dirvish-mode . (lambda () (dired-get-filename nil t)))) + (dirvish-mode . (lambda () (dired-get-filename nil t))) + (mu4e-view-mode . (lambda () + (when-let* ((msg (mu4e-message-at-point)) + (id (plist-get msg :message-id))) + (format "mu4e:msgid:%s" id)))) + (Info-mode . (lambda () + (when (and (boundp 'Info-current-file) + (boundp 'Info-current-node) + Info-current-file + Info-current-node) + ;; Strip the compression suffix (via + ;; file-name-base) AND the .info suffix. + ;; "emacs.info.gz" -> base "emacs.info" -> + ;; manual "emacs". + (let* ((base (file-name-base Info-current-file)) + (manual (if (string-suffix-p ".info" base) + (substring base 0 -5) + base)) + (node Info-current-node)) + (when (and (not (string-empty-p manual)) + (not (string-empty-p node))) + (format "info:(%s)%s" manual node))))))) "Alist mapping major-mode -> thunk returning the buffer's \"source\". Each thunk is called with no arguments and should return a string diff --git a/tests/test-custom-buffer-file-copy-buffer-source.el b/tests/test-custom-buffer-file-copy-buffer-source.el index c4e073ae..f4afd109 100644 --- a/tests/test-custom-buffer-file-copy-buffer-source.el +++ b/tests/test-custom-buffer-file-copy-buffer-source.el @@ -125,6 +125,60 @@ to `buffer-file-name'." (cj/copy-buffer-source-as-kill)) (should (equal (car kill-ring) "/home/u/books/manual.pdf"))))) +;;; mu4e-view-mode dispatch + +(ert-deftest test-copy-buffer-source-mu4e-view-copies-msgid-link () + "Normal: in mu4e-view-mode, copy a `mu4e:msgid:' link form. +The URL-shaped string pastes into org as a clickable link and +matches mu4e's own org-protocol handler." + (let (kill-ring) + (cl-letf (((symbol-function 'mu4e-message-at-point) + (lambda () (list :message-id "abc123@example.test" + :subject "Re: lunch")))) + (with-temp-buffer + (setq major-mode 'mu4e-view-mode) + (cl-letf (((symbol-function 'message) #'ignore)) + (cj/copy-buffer-source-as-kill)) + (should (equal (car kill-ring) "mu4e:msgid:abc123@example.test")))))) + +(ert-deftest test-copy-buffer-source-mu4e-view-without-message-falls-through () + "Boundary: when `mu4e-message-at-point' returns nil (e.g. point +isn't on a real message), the dispatcher falls back to +`buffer-file-name' rather than erroring." + (let (kill-ring) + (cl-letf (((symbol-function 'mu4e-message-at-point) (lambda () nil))) + (with-temp-buffer + (setq major-mode 'mu4e-view-mode + buffer-file-name "/tmp/fallback.eml") + (cl-letf (((symbol-function 'message) #'ignore)) + (cj/copy-buffer-source-as-kill)) + (should (equal (car kill-ring) "/tmp/fallback.eml")))))) + +;;; Info-mode dispatch + +(ert-deftest test-copy-buffer-source-info-mode-formats-as-org-info-link () + "Normal: in Info-mode, return `info:(manual)node' -- the form +`org-info-store-link' produces, which org renders as a clickable +link target." + (let (kill-ring) + (with-temp-buffer + (setq major-mode 'Info-mode) + (setq-local Info-current-file "/usr/share/info/emacs.info.gz") + (setq-local Info-current-node "Buffers") + (cl-letf (((symbol-function 'message) #'ignore)) + (cj/copy-buffer-source-as-kill)) + (should (equal (car kill-ring) "info:(emacs)Buffers"))))) + +(ert-deftest test-copy-buffer-source-info-mode-without-context-falls-through () + "Boundary: when Info hasn't populated `Info-current-file' or +`Info-current-node' (uninitialized), the dispatcher falls through +to `buffer-file-name' (here nil → user-error)." + (with-temp-buffer + (setq major-mode 'Info-mode) + (setq-local Info-current-file nil) + (setq-local Info-current-node nil) + (should-error (cj/copy-buffer-source-as-kill) :type 'user-error))) + ;;; Backwards-compat alias (ert-deftest test-copy-path-old-name-aliases-new-command () diff --git a/todo.org b/todo.org index da6eeb1f..25c93cb8 100644 --- a/todo.org +++ b/todo.org @@ -39,6 +39,7 @@ Tags are additive. For example, a small wrong-behavior fix can be * Emacs Open Work +** TODO [#B] Write spec on what's needed for music not to depend on EMMS ** DONE [#B] Update gptel models :chore: CLOSED: [2026-05-14 Thu] Anthropic side: bumped Opus 4.6 → 4.7 (current frontier); Sonnet 4.6 @@ -106,7 +107,8 @@ heading, =help-mode=, =Info-mode=, =magit-log-mode= / These need format decisions (Message-ID vs link vs subject, id link vs CUSTOM_ID vs heading text, etc.) before implementation. -** TODO [#C] Extend cj/buffer-source-functions to more modes :feature: +** DONE [#C] Extend cj/buffer-source-functions to more modes :feature: +CLOSED: [2026-05-15 Fri] Followup to =Modify C-; b p=. The first batch covered eww, elfeed-show, dired/dirvish, and doc-view/pdf-view (via the buffer-file-name fallback). These modes still need a decision + -- cgit v1.2.3