Troubleshooting
for some reason it gets stuck at the end of the From:
line and won't advance further to the message body
The docstrings of user option message-tab-body-function
:
Function to execute when ‘message-tab’ (TAB) is executed in the body.
If nil, the function bound in ‘text-mode-map’ or ‘global-map’ is executed.
and command message-tab
:
Complete names according to ‘message-completion-alist’.
Execute function specified by ‘message-tab-body-function’ when
not in those headers. If that variable is nil, indent with the
regular text mode tabbing command.
go some way towards explaining why your command message-mode-next-field
might get stuck.
As the name of message-tab-body-function
suggests, the function assigned to it is not necessarily intended to work or be called while point is within the headers, as opposed to the body, of the message.
In fact, inspecting the command's source
(defun message-tab ()
"Complete names according to `message-completion-alist'.
Execute function specified by `message-tab-body-function' when
not in those headers. If that variable is nil, indent with the
regular text mode tabbing command."
(interactive)
(cond
((let ((completion-fail-discreetly t))
(completion-at-point))
;; Completion was performed; nothing else to do.
nil)
(message-tab-body-function (funcall message-tab-body-function))
(t (funcall (or (lookup-key text-mode-map "\t")
(lookup-key global-map "\t")
'indent-relative)))))
reveals that message-tab-body-function
is only called when completion of header field values fails.
Emacs 25+ solution
Here are two sample commands for jumping back and forth between "fields" of interest, such as header values and the start of each of the message body and signature:
(autoload 'mail-hist-forward-header "mail-hist")
(autoload 'mail-text-start "sendmail")
(defun my-message-signature-start ()
"Return value of point at start of message signature."
(save-mark-and-excursion
(message-goto-signature)
(point)))
(defun my-message-field-forward ()
"Move point to next \"field\" in a `message-mode' buffer.
With each invocation, point is moved to the next field of
interest amongst header values, message body and message
signature, in that order."
(interactive)
(cond ((message-point-in-header-p)
(unless (mail-hist-forward-header 1)
(call-interactively #'message-goto-body)))
((>= (point) (my-message-signature-start))
(message "No further field"))
((message-in-body-p)
(message-goto-signature))
(t ; Probably on `mail-header-separator' line
(call-interactively #'message-goto-body))))
(defun my-message-field-backward ()
"Like `my-message-field-forward', but in opposite direction."
(interactive)
(cond ((or (message-point-in-header-p)
(<= (point) (mail-text-start)))
(unless (mail-hist-forward-header
(if (message-point-in-header-p) -1 0))
(message "No further field")))
((<= (point) (my-message-signature-start))
(call-interactively #'message-goto-body))
(t ; Beyond start of signature
(message-goto-signature))))
You can bind them to whichever keys you like, but assuming you want the tab key to jump forward and shift-tab to jump backward, you can write
(with-eval-after-load 'message
(define-key message-mode-map "\t" #'my-message-field-forward)
(dolist (key '(backtab S-tab S-iso-lefttab))
(define-key message-mode-map (vector key) #'my-message-field-backward)))
The dolist
form above handles all three varieties of shift-tab found in the wild, just in case. If you know which one your system uses, you can, of course, target that one specifically, or use a completely different binding altogether.
Caveats
Emacs includes a myriad of libraries for parsing, composing, encoding, etc. messages, with lisp/gnus/message.el
(a.k.a. message-mode
) being just one of them. Unfortunately, none of them are complete or consistent with one another. In other words, for many useful operations, each library reinvents the wheel, and in doing so often creates its own edge cases and bugs. Even libraries aiming to provide a common interface for all other libraries to use are incomplete or remain unused.
I say this as a disclaimer for the sample code above, which resorts to using two functions external to message-mode
: mail-hist-forward-header
from lisp/mail/mail-hist.el
and mail-text-start
from lisp/mail/sendmail.el
.
A message-mode
purist could, in fact, replace a call to mail-text-start
with
(save-mark-and-excursion
(message-goto-body)
(point))
or, since Emacs 27.1, simply
(save-excursion
(message-goto-body))
for free.
mail-hist-forward-header
, on the other hand, has no equal in any other library which I am aware of. In addition, the operation it performs is amenable to a relatively high number of edge cases, e.g. related to unexpected starting position or whitespace.
In general, however, and assuming your messages are well-formed and not too exotic, the provided code should work reasonably well.
Emacs 24 compatibility
The code above uses the function save-mark-and-excursion
, which was first introduced in Emacs 25. In versions of Emacs prior to that, the function can be replaced with save-excursion
. In order to simultaneously support Emacs 25 and earlier versions, you can replace save-mark-and-excursion
with your own compatibility function, e.g. my-save-mark-and-excursion
, which can be defined as
(defalias 'my-save-mark-and-excursion
(if (fboundp 'save-mark-and-excursion)
#'save-mark-and-excursion
#'save-excursion)
"Backport `save-mark-and-excursion' to Emacsen before 25.1.")
Emacs 23 compatibility
In addition to the Emacs 24 workaround, Emacs 23 further lacks a definition for with-eval-after-load
. The provided key binding code can thus be written as
(eval-after-load 'message
(lambda ()
(define-key message-mode-map "\t" #'my-message-field-forward)
(dolist (key '(backtab S-tab S-iso-lefttab))
(define-key message-mode-map (vector key) #'my-message-field-backward))))
instead.