20

I have an autocmd for TeX and Markdown files to save the file automatically. Nothing unusual:

autocmd CursorHold *.tex,*.md w

However, as custom settings for these files increased, I split them off into ftplugin/tex.vim and ftplugin/markdown.vim:

" ftplugin/tex.vim
autocmd CursorHold *.tex w
" ftplugin/markdown.vim
autocmd CursorHold *.md w

Now, these files are sourced only for the appropriate files, so the pattern matching is redundant. Apparently, autocmds can be buffer-local. From :h autocmd-buffer-local:

Buffer-local autocommands are attached to a specific buffer.  They are useful
if the buffer does not have a name and when the name does not match a specific
pattern.  But it also means they must be explicitly added to each buffer.

Instead of a pattern buffer-local autocommands use one of these forms:
        <buffer>        current buffer
        <buffer=99>     buffer number 99
        <buffer=abuf>   using <abuf> (only when executing autocommands)
                        <abuf>

That seems to be meant for such usage. Now, both ftplugin/tex.vim and ftplugin/markdown.vim can have:

autocmd CursorHold <buffer> w

I'm not really concerned about the actual extension as long as the filetype is correct, so this saves me from having to worry about *.md and *.markdown and whatever other extensions are valid for Markdown.

Is this usage of <buffer> correct? Are there any pitfalls I should be aware of? Will things get messy if I wipe a buffer and open another (hopefully the numbers won't collide, but …)?

5
  • 2
    I'm pretty sure if you wipe a buffer, any buffer-local autocmds get wiped too.
    – Tumbler41
    Jun 1, 2016 at 17:47
  • @Tumbler41 indeed. It does say that in the help, a few paragraphs down.
    – muru
    Jun 1, 2016 at 17:49
  • 1
    Not exactly what you asked, but up (short for :update) would be better than w in your autocmd (avoid unnecessary writes).
    – mMontu
    Jun 1, 2016 at 18:09
  • 1
    @mMontu Nice. It even solves a problem I had when the autocmd activated for an a file I was examining from git history. The buffer was readonly, and w failed. Repeatedly. :up doesn't do anything in that case. :)
    – muru
    Jun 1, 2016 at 18:11
  • Glad you liked :) Incidentally, you could also find the 'autowrite' option useful (depending on your motivation, you could drop the autocmds).
    – mMontu
    Jun 1, 2016 at 18:13

1 Answer 1

30

Is this usage of <buffer> correct?

I think it is correct, but you just need to wrap it inside an augroup, and clear the latter, to make sure that the autocmd won't be duplicated every time you execute a command which reloads the same buffer.

As you explained, the special pattern <buffer> allows you to rely on the built-in filetype detection mechanism, implemented inside the file $VIMRUNTIME/filetype.vim.

In this file, you can find the Vim's built-in autocmds which are responsible for setting the correct filetype for any given buffer. For example, for markdown:

" Markdown
au BufNewFile,BufRead *.markdown,*.mdown,*.mkd,*.mkdn,*.mdwn,*.md  setf markdown

Inside your filetype plugin, you could copy the same patterns for every autocmd you install. For example, to automatically save the buffer when your cursor hasn't moved during a few seconds:

au CursorHold *.markdown,*.mdown,*.mkd,*.mkdn,*.mdwn,*.md  update

But <buffer> is way less verbose:

au CursorHold <buffer> update

Besides, if some day another extension is valid, and $VIMRUNTIME/filetype.vim is updated to include it, your autocmds won't be informed. And you'll have to update all their patterns inside your filetype plugins.


Will things get messy if I wipe a buffer and open another (hopefully the numbers won't collide, but …)?

I'm not sure but I don't think that Vim can re-use the buffer number of a wiped buffer. I couldn't find the relevant section from the help, but I found this paragraph from vim.wikia.com:

No. Vim will not re-use the buffer number of a deleted buffer for a new buffer. Vim will always assign the next sequential number for a new buffer.

Besides, as @Tumbler41 explained, when you wipe a buffer, its autocmds are removed. From :h autocmd-buflocal:

When a buffer is wiped out its buffer-local autocommands are also gone, of course.

If you want to check yourself, you can do so by increasing Vim's verbosity level to 6. You can do it temporarily, just for one command, by using the :verbose modifier. So, inside your markdown buffer, you could execute:

:6verbose bwipe

Then, if you check Vim's messages:

:messages

You should see a line looking like this:

auto-removing autocommand: CursorHold <buffer=42>

Where 42 was the number of your markdown buffer.


Are there any pitfalls I should be aware of?

There are 3 situations which I would consider as pitfalls and which involve the special pattern <buffer>. In two of them, <buffer> may be an issue, in the other it's a solution.

Pitfall 1

First, you should be careful with the way you clear the augroups of your buffer-local autocmds. You must be familiar with this snippet:

augroup your_group_name
    autocmd!
    autocmd Event pattern command
augroup END

So, you could be tempted to use it for your buffer-local autocmds, unmodified, like this:

augroup my_markdown
    autocmd!
    autocmd CursorHold <buffer> update
augroup END

But this will have an undesired effect. The first time you load a markdown buffer, let's call it A, its autocmd will be correctly installed. Then, when you'll reload A, the autocmd will be deleted (because of autocmd!), and re-installed. So, the augroup will correctly prevent the duplication of the autocmd.

Now, suppose you load a 2nd markdown buffer, let's call it B, in a 2nd window. ALL the autocmds of the augroup will be cleared: that is the autocmd of A and the one of B. Then, a SINGLE autocmd will be installed for B.

So, when you make some change in B, and wait a few seconds for CursorHold to be fired, it will be automatically saved. But if you go back to A and do the same thing, the buffer won't be saved. This is because the last time you loaded a markdown buffer, there was an imbalance between what you removed and what you added. You removed more than what you added.

The solution is to not remove ALL autocmds, but only the ones of the current buffer, by passing the special pattern <buffer> to :autocmd!:

augroup my_markdown
    autocmd! CursorHold <buffer>
    autocmd CursorHold <buffer> update
augroup END

Note that you could replace CursorHold with a star to match any event in the line which removes autocmds:

augroup my_markdown
    autocmd! * <buffer>
    autocmd CursorHold <buffer> update
augroup END

This way, you don't have to specify all the events to which your autocmds are listening when you want to clear the augroup.


Pitfall 2

There's another pitfall, but this time <buffer> is not the issue, it's the solution.

When you include a local option in a filetype plugin, you probably do it like this:

setlocal option1=value
setlocal option2

This will work as expected for buffer-local options, but not always for window-local ones. To illustrate the issue, you could try the following experiment. Create the file ~/.vim/after/ftdetect/potion.vim, and inside it write:

autocmd BufNewFile,BufRead *.pn setfiletype potion

This file will automatically set the filetype potion for any file whose extension is .pn. You don't need to wrap it inside an augroup because, for this particular kind of file, Vim will do it automatically (see :h ftdetect).

If the intermediate directories don't exist on your system, you can create them.

Next, create the filetype plugin ~/.vim/after/ftplugin/potion.vim, and inside it write:

setlocal list

By default, in a potion file, this setting will cause the tab characters to be displayed as ^I and the end of lines as $.

Now, create a minimal vimrc; inside /tmp/vimrc write:

filetype plugin on

... to enable the filetype plugins.

Also, create a potion file, /tmp/pn.pn, and a random file /tmp/file. In the potion file, write something:

foo
bar
baz

In the random file, write the path to the potion file /tmp/pn.pn:

/tmp/pn.pn

Now, start Vim with a minimum of initializations, just sourcing the vimrc, and open both files in vertical viewports:

$ vim -Nu /tmp/vimrc -O /tmp/pn.pn /tmp/file

You should see 2 vertical viewports. The potion file on the left displays the end of lines with dollar signs, the random file on the right doesn't display them at all.

Give the focus to the random file, and press gf to display the potion file whose path is under the cursor. You now see the same potion buffer in the right viewport, but this time, the end of lines are not displayed with dollar signs. And if you type :setlocal list?, Vim should answer with nolist:

enter image description here

The whole chain of events:

BufRead event → set 'filetype' option → load filetype plugins

... didn't occur, because the first of them, BufRead, didn't occur when you pressed gf. The buffer was already loaded.

It may seem unexpected, because when you added setlocal list inside your potion filetype plugin, you may have thought that it would enable the 'list' option in any window displaying a potion buffer.

The issue is not specific to this new potion filetype. You can experience it with a markdown file too.

It's not specific to the 'list' option either. You can experience it with other window-local settings, like 'conceallevel', 'foldmethod', 'foldexpr', 'foldtitle', ...

It's not specific to the gf command either. You can experience it with other commands which may change the buffer displayed in the current window: global mark, C-o (move backward in the window-local jumplist), :b {buffer_number}, ...

To summarize, the window-local options will be correctly set, if and only if:

  • the file hasn't been read during the current Vim session (because BufRead will have to be fired)
  • the file is being displayed in a window where the window-local options were already correctly set
  • the new window is created with a command such as :split (in this case, it should inherit the window-local options from the window where the command was executed)

Otherwise, the window-local options may not be correctly set.

A possible solution would be to set them not directly from a filetype plugin, but from an autocmd installed in the latter, which would listen to BufWinEnter. This event should be fired every time a buffer is displayed in a window.

So, for example, instead of writing this:

setlocal list

You would write this:

augroup my_potion
    au! * <buffer>
    au BufWinEnter <buffer> setlocal list
augroup END

And here, you find again the special pattern <buffer>.

enter image description here


Pitfall 3

If you change the filetype of your buffer, the autocmds will remain. If you want to remove them, you need to configure b:undo_ftplugin (see :h undo_ftplugin), and include this command in it:

exe 'au! my_markdown * <buffer>'

However, don't try to remove the augroup itself, because there could still be some markdown buffers which have autocmds inside it.

FWIW, this is the UltiSnips snippet that I'm using to set b:undo_ftplugin:

snippet undo "undo ftplugin settings" bm
" teardown {{{1

let b:undo_ftplugin =         get(b:, 'undo_ftplugin', '')
\                     .(empty(get(b:, 'undo_ftplugin', '')) ? '' : '|')
\                     ."${1:
\                          setl ${2:option}<}${3:
\                        | exe '${4:n}unmap <buffer> ${5:lhs}'}${6:
\                        | exe 'au! ${7:group_name} * <buffer>'}${8:
\                        | unlet! b:${9:variable}}${10:
\                        | delcommand ${11:Cmd}}
\                      "
$0
endsnippet

And here's an example of value which I have in ~/.vim/after/ftplugin/awk.vim:

let b:undo_ftplugin =         get(b:, 'undo_ftplugin', '')
                    \ .(empty(get(b:, 'undo_ftplugin', '')) ? '' : '|')
                    \ ."
                    \   setl cms< cocu< cole< fdm< fdt< tw<
                    \|  exe 'nunmap <buffer> K'
                    \|  exe 'au! my_awk * <buffer>'
                    \|  exe 'au! my_awk_format * <buffer>'
                    \  "

As a side note, I understand why you asked the question, because when I looked for all the lines where the special pattern <buffer> was used in Vim's default files:

:vim /au\%[tocmd!].\{-}<buffer>/ $VIMRUNTIME/**/*

I only found 9 matches (you may find more or less, I'm using Vim version 8.0, with patches up to 134). And among the 9 matches, 7 are in the documentation, only 2 are actually sourced. You should find them in $VIMRUNTIME/syntax/dircolors.vim:

autocmd CursorMoved,CursorMovedI <buffer> call s:preview_color('.')
autocmd CursorHold,CursorHoldI   <buffer> call s:reset_colors()

I don't know if it can cause an issue, but they aren't inside an augroup, which means every time you reload a buffer whose filetype is dircolors (it happens if you edit a file named .dircolors, .dir_colors, or whose path ends with /etc/DIR_COLORS), the syntax plugin will add a new buffer-local autocmd.

You can check it like this:

$ vim ~/.dir_colors
:au * <buffer>

The last command should display this:

CursorHold
    <buffer=1>
              call s:reset_colors()
CursorHoldI
    <buffer=1>
              call s:reset_colors()
CursorMoved
    <buffer=1>
              call s:preview_color('.')
CursorMovedI
    <buffer=1>
              call s:preview_color('.')

Now, reload the buffer, and ask again what are the buffer-local autocmds for the current buffer:

:e
:au * <buffer>

This time, you will see:

CursorHold
    <buffer=1>
              call s:reset_colors()
              call s:reset_colors()
CursorHoldI
    <buffer=1>
              call s:reset_colors()
              call s:reset_colors()
CursorMoved
    <buffer=1>
              call s:preview_color('.')
              call s:preview_color('.')
CursorMovedI
    <buffer=1>
              call s:preview_color('.')
              call s:preview_color('.')

After every reloading of the file, s:reset_colors() and s:preview_color('.') will be called one additional time, every time one of the event CursorHold, CursorHoldI, CursorMoved, CursorMovedI is fired.

It's probably not a big issue, because even after reloading a dircolors file several times, I didn't see a noticeable slowdown, or unexpected behavior from Vim.

If it is an issue for you, you could contact the maintainer of the syntax plugin, but in the meantime, if you wanted to prevent the duplication of autocmds, you could create your own syntax plugin for dircolors files, using the file ~/.vim/syntax/dircolors.vim. Inside it you would import the contents of the original syntax plugin:

$ vim ~/.vim/syntax/dircolors.vim
:r $VIMRUNTIME/syntax/dircolors.vim

Then, in the latter, you would just wrap the autocmds inside an augroup which you would clear. So, you would replace these lines:

autocmd CursorMoved,CursorMovedI <buffer> call s:preview_color('.')
autocmd CursorHold,CursorHoldI   <buffer> call s:reset_colors()

... with these ones:

augroup my_dircolors_syntax
    autocmd! * <buffer>
    autocmd CursorMoved,CursorMovedI <buffer> call s:preview_color('.')
    autocmd CursorHold,CursorHoldI   <buffer> call s:reset_colors()
augroup END

Note that if you created your dircolors syntax plugin with the file ~/.vim/after/syntax/dircolors.vim, it wouldn't work, because the default syntax plugin would be sourced before. By using ~/.vim/syntax/dircolors.vim, your syntax plugin will be sourced before the default one, and it will set the buffer-local variable b:current_syntax, which will prevent the default syntax plugin from being sourced because it contains this guard:

if exists("b:current_syntax")
    finish
endif

The general rule seems to be: use the ~/.vim/ftplugin and ~/.vim/syntax directories to create a custom filetype / syntax plugin and prevent the next plugin (for the same filetype) in the runtime path to be sourced (including the default ones). And use ~/.vim/after/ftplugin, ~/.vim/after/syntax, not to prevent other plugins from being sourced, but just to have the last word on the value of some settings.

2
  • 2
    I wish I could upvote this harder.
    – Rich
    Nov 22, 2017 at 11:31
  • 5
    @Rich I upvoted this harder for you. My only complaint is the lack of a summary "tl;dr". Pages of textual minutiae, however vital to understanding, pain my aging soul to wade through. The replacement of autocmd! with autocmd! CursorHold <buffer> in augroup blocks is a particularly critical gotcha – and should have been highlighted upfront. Nonetheless... this is a blatantly amazing investment of time, effort, and bloody tears. Feb 7, 2018 at 7:00

Your Answer

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

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