3

I use message-mode to compose and send email. Rather than having to navigate manually through the fields, I would like to able to TAB from the To: field to the Subject: field, then TAB again to bring the point to the message body.

UPDATE. I got this partially working:

(defun message-mode-next-field ()
  (interactive)
  (next-line)
  (end-of-line)) 

(setq message-tab-body-function #'message-mode-next-field)

It works up to a point, but for some reason it gets stuck at the end of the From: line and won't advance further to the message body.

Basil
  • 12,019
  • 43
  • 69
incandescentman
  • 4,111
  • 16
  • 53
  • Since i don't have enough reputation to comment or Upvote or essentially do anything except posting... I thought i'd leave my feedback here... Huge Thanks to @Basil for the Post above! Except the Backward function getting stuck when it should jump into the header, everything works like a charm... I guess since this thread is over 8 years old it probably broke due to some updates or whatnot... Was playing around with this forever and could not figure it out... Now i can finally and easily jump to the next field in the message buffer on one simple button press <3 – Morpheus Jan 16 '23 at 15:20
  • @Morpheus Thanks for the feedback, and you're welcome. However, I'm currently running Emacs 30 and both `my-message-field-forward` and `my-message-field-backward` below still work fine for me. Could you perhaps describe in detail precisely which steps lead to the backward command getting stuck? Thanks. – Basil Jan 18 '23 at 11:57
  • @Morpheus Ah, I seem to have found a way to confuse `my-message-field-backward`: it relies on fields being well-formed, specifically of the form `FIELD:`. You can tell that a field name is well-formed when it is highlighted with `message-header-name`. If the trailing space is not present then the field name plus colon are not highlighted, and `my-message-field-backward` does not work as expected. If handling such cases is important to you, please create a new post and tag or send it to me, and I'll try my best to adapt the command to such edge cases. – Basil Jan 18 '23 at 12:01

2 Answers2

1

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.

Basil
  • 12,019
  • 43
  • 69
  • Interesting. But I get `Symbol’s function definition is void: message-goto-body-1` – incandescentman Jul 31 '17 at 22:36
  • @incandescentman Which version of Emacs are you running? I explain in my answer that `message-goto-body-1` is an Emacs 25 feature and that you can substitute `(message-goto-body-1)` with `(message-goto-body) (point)` in the provided code for Emacs versions 24 or older. – Basil Jul 31 '17 at 23:10
  • Emacs version 25.1.1 – incandescentman Jul 31 '17 at 23:13
  • @incandescentman Ah, I see now the function was introduced in a slightly more recent version of Emacs 25, specifically in January 2017. I will edit my answer accordingly shortly. Sorry about the confusion. – Basil Jul 31 '17 at 23:19
  • I changed it to `(message-goto-body) (point)` and that works. Is there a way to bind TAB so that it first tries to complete the name according to message-completion-alist, i.e. the default behavior of message-tab, and if completion fails, to then call `my-message-field-forward`? – incandescentman Jul 31 '17 at 23:21
  • @incandescentman See updated answer for the correct usage of `message-goto-body` and compatibility notes. – Basil Jul 31 '17 at 23:29
  • 1
    @incandescentman I see now that command `message-tab` has also been modified since your version of Emacs, where it deliberately forces completion to return a non-`nil` value so as to avoid falling back on `message-tab-body-function`. I do not have a solution for your desired behaviour at the moment. Please consider starting a new question for that and I will have another look later this week. – Basil Jul 31 '17 at 23:36
-2

C-c C-f C-s will move your cursor to the Subject: header.

db48x
  • 15,741
  • 1
  • 19
  • 23