5

I am writing a vimscript function which should keep in a consistent state a "home made buffer manager". The idea is that I want to keep a trace of my buffers in the tabs they were open in.

This "buffer manager" simply is a dictionary which has the numbers of the tabs as keys and lists of buffers numbers as values.

To add the different buffers to this dictionary I created the following autocommand:

autocmd! BufEnter * call AddBufferToTab()

And then I have my function:

function! AddBufferToTab()
    let newBufNr = bufnr("%") 

    " create an entry for the current tab if necessary
    if !has_key(g:BuffersManager, tabpagenr())
        let  g:BuffersManager[tabpagenr()] = []
    endif

    " Add the buffer to the tab
    if buflisted(newBufNr) && index(g:BuffersManager[tabpagenr()], newBufNr) == -1
        call add (g:BuffersManager[tabpagenr()],newBufNr)
    endif
endfunction

When vim enters a buffer the function is called, an empty list is created if the tab wasn't already managed.

Then I test if the buffer is listed by :ls with buflisted(newBufNr) and if the buffer is already in the list of the tab.

It is important to note that the first test seems to work: I don't want to add buffers which are not listed by :ls and when I open an help buffer for example, the buffer is not added

The function seems to work properly excepted for one use case:

When I open NERDTree with :NERDTree or :NERDTreeToggle the function doesn't seems to be executed (at least the debug echom that I used never appeared) and the buffer still end added to the manager.

Note that when I add

echom "ADD " . bufnr("%") . "  " . bufname("%") . "  " . buflisted(bufnr("%"))

To the function the informations seems to be the good ones (the ones of the currently entered buffer).

I also checked that NERDTree buffer have their property buflisted to 0.

I have been working on this problem for several days now and I really can't figure out what's happening.

TL;DR Why NERDTree buffers are added to the dictionary when the function is meant to ignore nobuflisted buffers?

NOTE If ones wants a big picture of the problem the function is a part of a plug-in I'm developing and which is available on github

NOTE 2 Of course if someone needs any additional information I'll gladly add them to this question, ask in the comments.


Additional information

I followed the comment of @VanLaser and splitted the if condition and based my test also on the name of the buffer. I also added some debugging message, here is the function in "debugging state":

function! AddBufferToTab()
    let newBufNr = bufnr("%") 

    " create an entry for the current tab if necessary
    if !has_key(g:BuffersManager, tabpagenr())
        let  g:BuffersManager[tabpagenr()] = []
    endif

    " Get conditions to add the tab
    let isListed = buflisted(newBufNr)

    let isAlreadyInManager = 1
    if (index(g:BuffersManager[tabpagenr()], newBufNr) == -1)
        let isAlreadyInManager = 0
    endif

    let isNERDTreeBuffer = 0
    if (bufname("%") =~ "NERD_Tree_")
        let isNERDTreeBuffer = 1
    endif

    " Debugging messages
    echom bufname("%")
    if (isListed)
        echom "is Listed"
    else 
        echom "is not Listed"
    endif

    if (isAlreadyInManager)
        echom "is AlreadyInManager"
    else 
        echom "is not AlreadyInManager"
    endif

    if (isNERDTreeBuffer)
        echom "is NERDTreeBuffer"
    else 
        echom "is not NERDTreeBuffer"
    endif
    echom "========="

    " Add the buffer to the tab
    if isListed && !isAlreadyInManager && !isNERDTreeBuffer
        call add (g:BuffersManager[tabpagenr()],newBufNr)
    endif
endfunction

I am sure that I don't have a call to the add function anywhere else than in this function.

I then followed this workflow:

  • sourcing the file, the buffer manager is empty
  • :e % (add the current file to the manager)
  • :e $MYVIMRC
  • :NERDTree
  • select the first file betterTabs.vim in NERDTree.

And here is the log I had:

enter image description here

As you can see the opening of NerdTree doesn't seem to trigger the function (since I don't have any message about it in the log) BUT when I output the content of my dictionnary I get:

enter image description here

As I said I don't have any other call to add in my whole file (I gave sooner in the post the link to the github repo if anyone wants to check).

I don't understand how the buffer can be added when it doesn't even trigger the function.


More infos to follow the investigations of @VanLaser in the comments (many thanks to him):

First, to quote :h ls:

Each buffer has a unique number. That number will not change, so you can always go to a specific buffer with :buffer N or N CTRL-^, where N is the buffer number.

So in theory if I get the number of the buffer I'm using it shouldn't change in the future.

Also I added the following lines at the very beginning of my function:

echom bufnr('%')
echom expand('<abuf>')

if bufnr('%') != expand('<abuf>')
    echom "RETURN"
    return
endif

and with the same workflow I get the same results, also I checked that the buffer numbers was the good ones:

Here is the output of messages:

enter image description here

The output of ls!:

enter image description here

And finally the content of my dictionary:

enter image description here

As you can see it seems that the numbers are correctly returned, but once again NERDTree is launched but doesn't trigger the function.


More infos to follow the comments of @lcd047

I changed the autocommand to

autocmd! BufEnter * call AddBufferToTab(expand('<abuf>'))

The function prototype to

function! AddBufferToTab(bufNumber)

and I replaced let newBufNr = bufnr("%") with let newBufNr = a:bufNumber and the behavior gets even weirder:

I added after the assignation of newBufNr the following debugging echos:

echom newBufNr
echom bufnr('%')
echom expand('<abuf>')
echom a:bufNumber

For every buffers they show the same number (so using <abuf> in the autocommand doesn't make a difference in the cases I tried).

But it get even weirder because then every buffers appears as not listed: for a reason that I don't understand buflisted(expand('<abuf>')) returns 0 for every buffers.


PS I'm sorry for the length of this post I'm just trying to give as much informations as possible because I really absolutely don't understand what's happening there.

  • 1
    Are you NerdTree buffers truly unlisted? Check :ls!. Do you get 0 when do :echo buflisted(bufnr('%')) inside the NerdTree buffer? You may also want to use has_key(g:MyDic, newBufNr). See :h has_key. Maybe it would be best to use expand('<abuf>') in your autocmd calls. See :h <abuf> – Peter Rincker Aug 12 '15 at 15:55
  • As I said in my question I debugged the buflisted property of nerdtree buffers and I confirm that the value of buflisted(bufnr('%')) for nerdtree buffers is 0. Also I didn't know :ls! but I get a u in front of those buffers, so know I'm really sure they are unlisted :-) Thanks for your other advices I'll definitely check them! – statox Aug 13 '15 at 9:24
  • I don't have NERDTree installed, so ... but if I can suggest another approach: leave the NERDTree buffers listed, and filter buffer addition in current tab list by the buffer's name. Also if you split the if in 2 parts, and assign those parts, each, to a variable, then you can check their values before the condition (if nonlisted && unmanaged ...). – VanLaser Aug 15 '15 at 11:55
  • 1
    @VanLaser: I thought of filtering the buffers according to their name but I think that might be less robust than the buflisted parameter: for now I've had problem only with NerdTree, but I'm not sure I will not have the same issue with another plugin and adding a condition for each case might be not very convenient. But if I don't find another solution that's probably what I'll do. Also yes when I'm debugging I split the if I put it back together for the question. – statox Aug 15 '15 at 12:03
  • Perhaps you add a buffer number for a certain buffer, but afterwards that number points to another buffer (e.g. the NERDTree one) - which would suggest some add/delete management issues (de-sync). – VanLaser Aug 15 '15 at 23:23
3
+100

tl;dr

You cannot filter the buffer at its creation, you should wait until you have more informations.

First attempt

I used the :debug NERDTree command to see all the calls that this function makes. When trying to stop it before the end I noticed something : A buffer was created, without name, without filetype. And it had the position/size of the nerdtree buffer (left split about 20 columns width).

It got me thinking that maybe you are trying to filter a buffer before it get's enough informations. So the buffer doesn't know that it will be a nerdtree buffer yet.

Second attempt

Instead of debugging manually I did a little log function that allows me to see the flow of events when calling :NERDTree :

augroup DebugGroup
  autocmd!
  autocmd BufAdd      * :call s:Debug('BufAdd')
  autocmd BufCreate   * :call s:Debug('BufCreate')
  " idem for BufDelete BufWipeout BufFilePre BufFilePost BufEnter BufLeave BufWinEnter
  "          BufWinLeave BufUnload BufHidden BufNew FileType Syntax
augroup END

function! s:Debug(message) abort
  silent execute '!echo '.a:message.' '.bufnr("%").' >> debug'
endfunction

Sample result :

BufNew 1
BufAdd 1
BufCreate 1
BufLeave 1
BufEnter 5
BufWinEnter 5
BufDelete 5
BufWipeout 5
BufUnload 5
BufNew 5
BufAdd 5
BufCreate 5
BufEnter 5
BufWinEnter 5
BufDelete 5
Syntax 5
FileType 5

You can see here that the buffer 1 is the original buffer and the 5 is the nerdtree one. And to create the nerdtree buffer, there are : two times the events BufEnter, BufDelete and BufWinEnter. It's kind of strange why nerdtree needs to open and delete two times the buffer to get working (but I'm sure they have a good reason).

Third attempt

This time I used the methods that you provide in your script and in your question to add details to the log :

function! s:DebugBuffer(event)
    let newBufNr = bufnr("%")
    let isListed = buflisted(newBufNr)
    let isAlreadyInManager = (index(g:BuffersManager[tabpagenr()], newBufNr) == -1)?0:1
    let isNERDTreeBuffer = (bufname("%") =~ "NERD_Tree_")?1:0
    let newBufSyntax = &ft

    call s:Debug('"'.a:event.' ['.newBufNr.'/'.bufname("%").' '.newBufSyntax.'] listed : '.isListed.' / managed : '.isAlreadyInManager.' / NERDTree '.isNERDTreeBuffer.'"')
    call s:Debug('"=> '.isListed.' & '.!isAlreadyInManager.' & '.!isNERDTreeBuffer.'"')
endfunction

augroup BuffersManagerGroup
  autocmd!
  autocmd BufAdd      * :call s:DebugBuffer('BufAdd')
  autocmd BufCreate   * :call s:DebugBuffer('BufCreate')
  " idem for BufDelete BufWipeout BufFilePre BufFilePost BufEnter BufLeave BufWinEnter
  "          BufWinLeave BufUnload BufHidden BufNew FileType Syntax
augroup END

function! s:Debug(message) abort
  silent execute '!echo '.a:message.' >> debug'
endfunction

The syntax is the following :

Event [BufferNumber/Filename FileType] listed : isListed / managed : isManaged / NERDTree : isNERDTree
=> litteral condition to be tested

The isNERDTree is checked using the name, as you did in your question.

The log is :

BufNew [4/betterTabs.vim vim] listed : 1 / managed : 0 / NERDTree 0
=> 1 & 1 & 1
BufAdd [4/betterTabs.vim vim] listed : 1 / managed : 1 / NERDTree 0
=> 1 & 0 & 1
BufCreate [4/betterTabs.vim vim] listed : 1 / managed : 1 / NERDTree 0
=> 1 & 0 & 1
BufLeave [4/betterTabs.vim vim] listed : 1 / managed : 1 / NERDTree 0
=> 1 & 0 & 1
BufEnter [5/ ] listed : 1 / managed : 0 / NERDTree 0
=> 1 & 1 & 1
BufWinEnter [5/ ] listed : 1 / managed : 1 / NERDTree 0
=> 1 & 0 & 1
BufDelete [5/ ] listed : 1 / managed : 1 / NERDTree 0
=> 1 & 0 & 1
BufWipeout [5/ ] listed : 1 / managed : 1 / NERDTree 0
=> 1 & 0 & 1
BufUnload [5/ ] listed : 1 / managed : 1 / NERDTree 0
=> 1 & 0 & 1
BufNew [5/NERD_tree_1 ] listed : 1 / managed : 1 / NERDTree 1
=> 1 & 0 & 0
BufAdd [5/NERD_tree_1 ] listed : 1 / managed : 1 / NERDTree 1
=> 1 & 0 & 0
BufCreate [5/NERD_tree_1 ] listed : 1 / managed : 1 / NERDTree 1
=> 1 & 0 & 0
BufEnter [5/NERD_tree_1 ] listed : 1 / managed : 1 / NERDTree 1
=> 1 & 0 & 0
BufWinEnter [5/NERD_tree_1 ] listed : 1 / managed : 1 / NERDTree 1
=> 1 & 0 & 0
BufDelete [5/NERD_tree_1 ] listed : 0 / managed : 1 / NERDTree 1
=> 0 & 0 & 0
Syntax [5/NERD_tree_1 nerdtree] listed : 0 / managed : 1 / NERDTree 1
=> 0 & 0 & 0
FileType [5/NERD_tree_1 nerdtree] listed : 0 / managed : 1 / NERDTree 1
=> 0 & 0 & 0

Here you can clearly see something : the new buffer (number 5) does not get noticed by the isNERDTree condition when triggering the BufEnter event, in fact it is not a nerdtree buffer yet. Later, on triggering the BufNew event its name change (but not its filetype).

Based on your primary conditions :

if isListed && !isAlreadyInManager && !isNERDTreeBuffer
    call add (g:BuffersManager[tabpagenr()],newBufNr)
endif

The add get called only one time : on the BuffEnter (notice the 1 & 1 & 1). When called on BufEnter, the buffer is added to the list, but after, because it already got considered, it doesn't change.

Once you know this you can do multiple things :

  1. On events, you check if the buffer should be removed, like so :

new condition :

if isListed && isAlreadyInManager && isNERDTreeBuffer
  " should be removed
endif
  1. You can use a different condition, instead of checking the name. As listed in my log, you can recognize for the filetype and on the FileType event delete the buffer.

Conclusion

For the nerdtree plugin, you should double check the validity of the buffer. As you cannot filter the buffer on it's creation, you should wait for another event to remove the new buffer if needed.

Notes :

  • Sorry for the long post, I wanted to be thorough.
  • If you want more details, feel free to ask, I'll be happy to add them.
  • Nice and complete answer, thanks for the deep investigation! I'll probably see to add the double check + remove mechanism. – statox Aug 17 '15 at 13:58
  • I'm trying to override the behavior of the J and K binds in nerdtree, as they are overridden by nerdtree to hop to the top and bottom of a directory. obviously the BufEnter map doesn't work, though it does after i switch out of and back into the nerdtree window. Is there a way to do this on the first open without lots of awful scripting? – Steven Lu Aug 3 '18 at 19:39

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.