Not everyone considers setting b:undo_ftplugin to be a "best practice" the way I do, but as far as I can tell, the behavior described in this question depends on it being set as part of the autocommand. This isn't an unreasonable thing to do, particularly if one religiously sets it, like I do.

I've been working on a plugin (call it foo) that uses multiple filetypes to enhance the editing experience of files in a particular path while still leveraging all the goodies that are setup by their respective filetype plugins.

More concretely, the plugin sets the filetype of any file in this path to foo.original_filetype. Vim's filetype plugin mechanism will use filetype plugins (and filetype autocommands) for both the foo and original_filetype filetypes in this case:

When a dot appears in the value then this separates two filetype names.

Example: /* vim: set filetype=c.doxygen : */

This will use the "c" filetype first, then the "doxygen" filetype. This works both for filetype plugins and for syntax files. More than one dot may appear.

-- :help 'filetype'

Now, I ran into a bizarre situation while experimenting with this plugin. For reasons™, I was keeping some extra foo filetype settings separate from my usual config, so I had done something like this:

" in a personal file sourced at the top of my vimrc
augroup FooStuff
  autocmd FileType *foo* nnoremap <buffer> f :echo "foo!"<CR>
  autocmd FileType *foo* let b:undo_ftplugin = 'nunmap <buffer> f'
augroup END

(Matching *foo* is necessary, since the entire dotted-filetype is matched against.)

Now, if I enter vim and :set filetype=foo.markdown, and then :map <buffer>, I get

n  \u3         *@:call UnderlineHeading(3)<CR>
n  \u2         *@:call UnderlineHeading(2)<CR>
n  \u1         *@:call UnderlineHeading(1)<CR>
n  \o          *@:silent !open %<CR>:redraw!<CR>
o  i#3         *@:<C-U>execute "normal! ?^###\\s.?e\r\rvg_"<CR>
o  i#2         *@:<C-U>execute "normal! ?^##\\s.?e\r\rvg_"<CR>
o  i#1         *@:<C-U>execute "normal! ?^#\\s.?e\r\rvg_"<CR>
o  ih-         *@:<C-U>execute "normal! ?^--\\+$\r\rkvg_"<CR>
o  ih=         *@:<C-U>execute "normal! ?^==\\+$\r\rkvg_"<CR>

Those are all markdown mappings, and f is conspicuously absent. What gives?

  • 1
    Do you really have use cases where you change the type of the current buffer? (I must admit that I define so many things in my C++ ftplugins, and as I have no ft-changing use case, that I've never took the time to set b:undo_ftplugin) May 29, 2020 at 15:27
  • @LucHermitte happens all the time when I’m experimenting, yeah. Plus, for this plugin, what if someone wants to remove the “extra” filetype for a particular file?
    – D. Ben Knoble
    May 29, 2020 at 16:46

1 Answer 1


It turns out, there is a subtle interaction between FileType events and the actual ftplugin mechanism.

When the filetype is changed, all matching FileType events are run. As near as I can tell, this is one of the first things to be done.

So the current "state" would include both the buffer-local mapping and the b:undo_ftplugin necessary to undo it. This will be relevant for the next step.

When vim has filetype plugin on, it uses ftplugin.vim in runtimepath to set up the following autocommand:

augroup filetypeplugin
  au FileType * call s:LoadFTPlugin()

  func! s:LoadFTPlugin()
    if exists("b:undo_ftplugin")
      exe b:undo_ftplugin
      unlet! b:undo_ftplugin b:did_ftplugin

    let s = expand("<amatch>")
    if s != ""
      if &cpo =~# "S" && exists("b:did_ftplugin")
    " In compatible mode options are reset to the global values, need to
    " set the local values also when a plugin was already used.
    unlet b:did_ftplugin

      " When there is a dot it is used to separate filetype names.  Thus for
      " "aaa.bbb" load "aaa" and then "bbb".
      for name in split(s, '\.')
    exe 'runtime! ftplugin/' . name . '.vim ftplugin/' . name . '_*.vim ftplugin/' . name . '/*.vim'
augroup END

The important thing here is that the function that loads all the filetype plugin files (like the one for markdown in the example) will start by undoing settings using b:undo_ftplugin. As you have no doubt realized, this undoes the mapping setup in the first autocommand.

While you could not set b:undo_ftplugin in the first autocommand to fix this, that feels dirty if you already like to set it. Instead, things will work just fine if you put your settings in a proper ftplugin file (as I have recommended in numerous places on this site; shame on me for not taking my own advice).

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.