2

The BufWinLeave event is not trigger when the buffer is still visible in another window, which includes the case when it is shown in a window in another tab. This is a annoying since only visible in the current tab is truly visible; and you cannot see a buffer in the screen when it is only "displayed" in another tab.

I want an autocmd to be executed when a buffer goes invisible from the screen (current tab). What is the best approach to achieve this?


Let me make it more specific. I want an autocmd for this

  1. a window's (W) buffer (B1) is changed to another one (B2) or that window is destroyed;
  2. B1 is not displayed in other windows of the current tabpage

This is equivalent to say:

  • B1 disappears from the current tabpage (i.e. the screen)

Note that if the whole current tabpage is closed, the entering tabpage becomes the current tabpage. So "disappears from the screen" is the more accurate statement.

I do not care about whether B1 is shown in other tabpages; and it should not be triggered when the cursor moves between splits of the current tabpage, like what BufLeave and WinLeave do.

Originally, I thought this might be described by BufWinLeave, but I realized today that BufWinLeave does not work like this. BufWinLeave is not triggered when the buffer is displayed in another tabpage.

Now, in my understanding, BufWinLeave works exactly the same as BufHidden:

  • when a buffer turns to be not shown in any of the window in any tabpage.

Please tell me if my understanding is wrong.

  • The TabLeave autocommand? – Christian Brabandt Nov 27 '18 at 11:44
  • That is not I am looking for. The tab is not changed. I am doing simple change of buffer like :b3 to switch out the current buffer. – Liu Sha Nov 27 '18 at 12:05
  • 1
    Well, that is not clear from the description of the problem. Perhaps try the BufLeave autocommand then. – Christian Brabandt Nov 27 '18 at 12:16
  • @ChristianBrabandt, thank you for your help. I updated the description. Please answer according to that if you have any thoughts. – Liu Sha Nov 27 '18 at 13:45
2

When you close the window of a buffer B1 which is still displayed in another tabpage, QuitPre, BufLeave and WinLeave are fired in the context of B1. Then, WinEnter and BufEnter are fired in the context of the buffer where the focus goes (B2). None of these events matches your description:

B1 disappears from the current tabpage (i.e. the screen)

However, you could listen to BufLeave to:

  • capture the number of the current buffer (B1) and the one of the current tabpage
  • install a fire-once autocmd listening to BufEnter to, later, check whether B1 is still displayed in the current tabpage

And when B1 does become invisible, you could fire a custom BufInvisible event (using :doautocmd, see :h :do).

Finally, you could listen to this new event to execute an arbitrary command.


As an example:

augroup will_buffer_become_invisible
    au!
    au BufLeave * call s:will_buffer_become_invisible()
augroup END

fu! s:will_buffer_become_invisible() abort
    let s:bufnr = bufnr('%')
    let s:tabnr = tabpagenr()
    augroup has_buffer_become_invisible
        au!
        au TabClosed * exe 'au! has_buffer_become_invisible'
            \ | aug! has_buffer_become_invisible
        au BufEnter * call s:has_buffer_become_invisible()
            \ | exe 'au! has_buffer_become_invisible'
            \ | aug! has_buffer_become_invisible
    augroup END
endfu

fu! s:has_buffer_become_invisible() abort
    let other_winids = filter(win_findbuf(s:bufnr), {i,v -> get(get(getwininfo(v), 0, {}), 'tabnr', 0) == s:tabnr})
    if empty(other_winids) && exists('#User#BufInvisible')
        do <nomodeline> User BufInvisible
    endif
endfu

To listen to BufInvisible, you would write something like:

augroup test_BufInvisible
    au!
    au User BufInvisible echom 'a buffer has become invisible'
augroup END

In #User#BufInvisible, is (1) User interpreted as group and BufInvisible as event; or (2) User as event and BufInvisible as pattern? (see the syntax for exists()).

Technically, User is an event. It's listed among other events at :h User, and the help refers to it as an event at :h :do:

You probably want to use <nomodeline> for events that are not used when loading a buffer, such as |User|.

It's a special kind of event, because it's never fired by Vim automatically. You have to fire it yourself by executing :doautocmd. It allows to create your own event. As a result, BufInvisible is a pattern in:

  • exists('#User#BufInvisible')
  • :do <nomodeline> User BufInvisible
  • au User BufInvisible

Although, if you look at :h :do, what follows the event is not a pattern, but a file name:

:do[autocmd] [<nomodeline>] [group] {event} [fname]
                                            ^^^^^^^

But here, BufInvisible is just a pattern, not a file name. It's matched against the name of the custom User event which has been fired.


If BufInvisible is the not a pattern, can I add a pattern to the au? I do not know the syntax for au User.

BufInvisible is the pattern, so you can't add another one after :au. The syntax for :au User is:

au User <your custom event> <your command>

As an example, if your custom event is MyNewEvent, and you want to log a message in :messages whenever it's fired, you could write:

au User MyNewEvent echom 'MyNewEvent has been fired'

why do you use exe 'au! has_buffer_become_invisible' instead of au! has_buffer_become_invisible? Is it because au handles | by itself.

You're right, :autocmd considers the bar as being part of its argument, and can't be followed by another Vim command. It's explained at :h :bar.

The :execute prevents :autocmd from seeing what's outside the string, so it should leave :aug! alone. The purpose of the latter is to remove the augroup, once :au! has cleared it (you can't remove an augroup when it's not empty; see :h :augroup-delete).

  • Thanks. This seems feasible. I will have some test later. I have a few questions on the code: In #User#BufInvisible#, is (1) User interpreted as group and BufInvisible as event; or (2) User as event and BufInvisible as pattern? (see the syntax for exists()). If BufInvisible is the not a pattern, can I add a pattern to the au? I do not know the syntax for au User – Liu Sha Nov 28 '18 at 1:26
  • why do you use exe 'au! has_buffer_become_invisible' instead of au! has_buffer_become_invisible? Is it because au handles | by itself? – Liu Sha Nov 28 '18 at 1:29
  • thank you for the detailed explanation. Here I feel that the tabnr handling is not so correct. If the tabpage is closed completely, it will check the wrong tabpage. Is there a concept of tabpage id? I once asked another question about TabClose, which you may be able to answer. – Liu Sha Nov 28 '18 at 2:25
  • You're right, the code doesn't properly handle the case where a tabpage is closed. Between BufLeave and BufEnter, Vim should fire the TabLeave, TabClosed and TabEnter events. I'm not sure how you want Vim to behave in such a case. The way I understood your request, I assumed that your definition of the event was: the buffer disappears from the current tabpage. When you close the current tabpage, the latter changes, so I wouldn't check whether the buffer is displayed in the new tabpage, because it's not the same anymore, and I would just cancel the BufInvisible event. – user938271 Nov 28 '18 at 2:55
  • That's my understanding which may differ from yours. Anyway, to cancel the event, I'm not sure it would work, but you could install an autocmd listening to TabClosed, and use it to clear the augroup before the autocmd listening to BufEnter has a chance to be processed. I edited the code accordingly. As for the concept of a tabpage id, unfortunately I don't think it exists. If it did, it would probably be present inside the list keys(getwininfo()[0]). – user938271 Nov 28 '18 at 2:55
1

Based on @user938271's answer, I slightly changed the code so that it is more concise:

augroup will_buffer_become_invisible
    au! BufLeave * call s:will_buffer_become_invisible()
augroup END

fu! s:will_buffer_become_invisible() abort
    augroup has_buffer_become_invisible
        exe printf('au! BufEnter * call s:has_buffer_become_invisible(%d)', bufnr('%'))
    augroup END
endfu

fu! s:has_buffer_become_invisible(bufnr)
    try
        let other_wins = filter(range(1, winnr('$')), {i,v->winbufnr(v) == a:bufnr})
        if empty(other_wins) && exists('#User#BufInvisible')
            do <nomodeline> User BufInvisible
        endif
    finally
        au! has_buffer_become_invisible
        aug! has_buffer_become_invisible
    endtry
endfu

Note: here if you close a tabpage it will check whether the entering tabpage will have the buffer shown.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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