I'd like to use ftplugin/mail.vim to execute a normal command after opening an email in Mutt.

Specifically, when replying to an email, I'd like move the cursor after the header fields (I have set edit_headers in my muttrc) and before the text of the email I reply to:

From: Me <[email protected]>
To: Recipient <[email protected]>
Subject: Re: The mail subject
In-Reply-To: <id@example>

On 2021-01-01 at 09:13:16, Recipient ([email protected]) wrote:
> The email
> I'm
> replying to

In other words, I want to jump to the empty line after In-Reply-To: <id@example>.

I know that the normal command } should suffice to jump over the paragraph of headers, thus I added this to ftplugin/mail.vim:

normal }

Unfortunately, that takes me to the bottom of the buffer, not to the empty line.

This works though:

  • Adding normal } to after/syntax/mail.vim instead of ftplugin/mail.vim. I could go with that, but it seems to me that normal } doesn't belong in after/syntax/mail.vim
  • Adding normal } to after/ftplugin/mail.vim instead of ftplugin/mail.vim.
  • Adding this to my muttrc: set editor="vim -c 'normal }'". Like above, I feel it would be cleaner to add that command to ftplugin/mail.vim

My questions:

  1. Is ftplugin/mail.vim the right place for normal commands?
  2. If so, how can I jump to the first empty line in an email?

I'm using Neovim v0.4.4 on Arch Linux 5.9.14.

I encountered some strange behavior while testing normal commands: When adding normal iABCDE to ftplugin/mail.vim I see "ABCDABCDEE" is written to the file. As if ftplugin/mail.vim was executed twice.

Cause of the issue

I figured out the issue thanks to filbranden's answer.

My ftplugin/mail.vim was indeed sourced twice due to this in my init.vim (Neovim's equivalent to vimrc):

let vimDir = '$XDG_CONFIG_HOME/nvim'
let &runtimepath.= ',' . vimDir

The problem was the second line that appended my config directory, effectively making ~/.config/nvim appear at the beginning and the end of the runtime path.


Although using normal } in ftplugin/mail.vim works now, I still put my mail config in after/ftplugin/mail.vim since I want to override the textwidth config in /usr/share/nvim/runtime/ftplugin/mail.vim. The contents of after/ftplugin/mail.vim are now:

setlocal textwidth=0
" Thanks for the tip, B Layer
normal! }
  • 1
    Did you know that normal! is almost always the better command to use than normal due to the fact that the former will ignore any mappings you've defined that happen to appear in the arguments? May or may not be the reason for } not going where you expect but regardless it's good to keep in mind.
    – B Layer
    Commented Jan 1, 2021 at 11:58
  • Your problem is due to your config. So dig it. My guess is :h restore-cursor (perhaps, from :h defaults.vim).
    – Matt
    Commented Jan 1, 2021 at 13:04

1 Answer 1


Interesting situation. Let's first address debugging it. It seems your experience with adding a normal iABCDE to the .vim/ftplugin/mail.vim and seeing it twice really suggests this file is being sourced more than once.

I think one thing you can try is adding statements that can display where &filetype was set and also show the stack trace of the function call to that file, to see what is happening:

echomsg execute('verbose set filetype?')
echomsg expand('<stack>')

Though the fact that this is only happening with .vim/ftplugin/mail.vim but not with .vim/after/ftplugin/mail.vim suggests that something else is afoot.

Take a close look at &runtimepath (with :echo &rtp or perhaps :put =&rtp to append it to a buffer so you can easily split it on commas) and see if ~/.vim is somehow listed twice, or if there's a second directory listed that happens to be a symlink pointing back to the same ~/.vim.

When loading files with :runtime! ftplugin/mail.vim, Vim will load all files that match that pattern. If Vim finds the same file multiple times, it's possible that it's sourcing it twice for that reason.

(Also check out :scriptnames, which will tell you all the paths of scripts that have been loaded.)

As for the best way to accomplish this configuration, I'd say a later event than FileType is probably better.

You might not be currently sourcing defaults.vim, but if you do, it includes a snippet that tries to restore cursor position when opening a file that might end up interfering with your setting. Some Linux distributions also ship a similar snippet in the system-wide vimrc file. If you do source the defaults.vim file (or otherwise if you have no vimrc file, for example if you move all settings, mappings and autocmd's to plugin files), then it will happen after the FileType event and may really interfere.

A good option is to use the BufEnter event instead, which happens only once when the buffer is first loaded, but it happens after all the BufRead (same as BufReadPost) and FileType events are processed. That way you know all setup is already done.

My recommendation here would be to use the ftplugin to set up a buffer-local autocmd that executes your action at the appropriate moment.

Also, instead of normal! }, you can use an Ex command directly for this motion. Using a simple address in Ex means to move to that line, and the 0;'} address means just that: Find the first blank line, starting from the beginning of the file. I'd also recommend using the keepjumps modifier, to prevent this motion from counting as a jump (just a small bit of hygiene, even if not super important here.)

Putting it all together, add the following line to your ftplugin/mail.vim (in either .vim or .vim/after):

autocmd BufEnter <buffer> ++once keepjumps 0;'}

If you want to go to the line below the first blank line, then 0;'}+1 would do that.

(See the relevant help articles for all entries above if you're not sure how this works. :help cmdline-ranges will explain ranges in Ex commands in more detail, and :help :[range] explains what happens if the range is the only thing in the command-line.)


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.