Skip to content

Improve buffer MRU tracking algorithm#121

Merged
jlanzarotta merged 18 commits intojlanzarotta:masterfrom
drmikehenry:mru
Mar 5, 2025
Merged

Improve buffer MRU tracking algorithm#121
jlanzarotta merged 18 commits intojlanzarotta:masterfrom
drmikehenry:mru

Conversation

@drmikehenry
Copy link
Copy Markdown
Contributor

Overview

This pull request provides a new MRU buffer tracking algorithm to:

  • Fix issue Bufexplorer MRU sort doesn't work properly if using restore_session.vim #8 "Bufexplorer MRU sort doesn't work properly if using restore_session.vim".

  • Fix issue Bufexplorer interferes with the startinsert! command #87 "Bufexplorer interferes with the startinsert! command".

  • Treat unlisted buffers as least-recently-used instead of most-recently-used in the MRU sort order.

  • Remove the now-unneeded status indicator "One tab/buffer" in the BufExplorer status line; also remove the associated undocumented variable g:bufExplorerOnlyOneTab.

  • Replace the undocumented m key (intended for debugging MRU logic) with the BufExplorer_eval() and BufExplorer_execute() general debugging features.

  • Improve speed of MRU tracking.

Details

  • For demonstration purposes, create a temporary directory for testing and create a few test files:

    mkdir testing
    cd testing
    touch file{1..500}.txt
    printf '%s:1:1\n' file{1..500}.txt > 500files.out
    printf '%s:1:1\n' file{1..9}.txt > 9files.out
    

    500files.out and 9files.out have simulated compiler output suitable for use with :cgetfile; for example, 9files.out is:

    file1.txt:1:1
    file2.txt:1:1
    file3.txt:1:1
    file4.txt:1:1
    file5.txt:1:1
    file6.txt:1:1
    file7.txt:1:1
    file8.txt:1:1
    file9.txt:1:1
    
  • MRU tracking is now done only on-demand as buffers come and go, so there is no need to track buffers at plugin startup or session-load time. This resolves two issues:

    • Issue Bufexplorer MRU sort doesn't work properly if using restore_session.vim #8 "Bufexplorer MRU sort doesn't work properly if using restore_session.vim" was caused by resetting the MRU list via s:Reset() after a session is loaded. Removing the SessionLoadPost autocmd that invokes this function resolves this issue.

    • Issue Bufexplorer interferes with the startinsert! command #87 "Bufexplorer interferes with the startinsert! command" was caused, as correctly diagnosed by the original poster, by the use of :normal! in the s:CatalogBuffers() function:

      silent execute 'normal! ' . tab . 'gt'

      s:CatalogBuffers() is no longer called at startup, resolving this issue.

  • Unlisted buffers (as created by :grep or :cgetfile, for example) are now treated as least-recently-used instead of most-recently-used in the MRU sort order. For example, launch Vim and run these commands:

    :edit file1.txt
    :edit file2.txt
    :cgetfile 9files.out

    Then launch BufExplorer and press u to view unlisted buffers.

    • Using BufExplorer 7.6.0, this results in:

      " Press <F1> for Help
      " Sorted by mru | Locate buffer | Show unlisted | One tab/buffer | Absolute Split path | Show terminal
      "=
        2 %a    file2.txt ~/testing line 1
        3u      file3.txt ~/testing line 0
        4u      file4.txt ~/testing line 0
        5u      file5.txt ~/testing line 0
        6u      file6.txt ~/testing line 0
        7u      file7.txt ~/testing line 0
        8u      file8.txt ~/testing line 0
        9u      file9.txt ~/testing line 0
        1 #     file1.txt ~/testing line 1
      

      Note that the unlisted buffers 3-9 (which have never been opened) are found between buffers 1 and 2 (which were used most recently).

    • Using the mru branch, unlisted buffers are shown last:

      " Press <F1> for Help
      " Sorted by mru | Locate buffer | Show unlisted | Absolute Split path | Show terminal
      "=
        2 %a    file2.txt ~/testing line 1
        1 #     file1.txt ~/testing line 1
        3u      file3.txt ~/testing line 0
        4u      file4.txt ~/testing line 0
        5u      file5.txt ~/testing line 0
        6u      file6.txt ~/testing line 0
        7u      file7.txt ~/testing line 0
        8u      file8.txt ~/testing line 0
        9u      file9.txt ~/testing line 0
      
  • The undocumented variable g:bufExplorerOnlyOneTab has been removed. Commit 3c0b11f (2017-09-18) removed the B key that permitted toggling this variable (via the s:ToggleOnlyOneTab() function). The variable controlled whether MRU data for a buffer would be gathered globally or on a per-tab basis (and defaulting to per-tab collection). In the new MRU algorithm, this control is not needed because MRU information is gathered and kept independently of whether buffers will be subsequently displayed globally or on a per-tab basis. As a result, the now-unneeded status indicator "One tab/buffer" has been removed, saving space in the BufExplorer status line.

  • The undocumented m key (intended for debugging MRU logic) has been removed and replace with more general BufExplorer_eval() and BufExplorer_execute() debugging features. These can be used to view or modify script-local variables and invoke script-local functions from outside the plugin. For example:

    • To display the MRU data structure s:bufMru:

      :echo BufExplorer_eval('s:bufMru')
      
    • To use s:MRUGetItems() to display buffer numbers from this structure in MRU-order:

      :echo BufExplorer_eval('s:MRUGetItems(s:bufMru,0)')
      
    • To re-initialize s:bufMru:

      :call BufExplorer_execute('let s:bufMru = s:MRUNew(0)')
      
  • The new MRU algorithm gathers buffer MRU data using a doubly linked list to avoid linear searching through arrays, and it defers display-related computations until BufExplorer has been invoked, improving MRU data collection speed. To demonstrate, launch Vim via:

    vim file*.txt
    

    First, visit all files in the argument list to ensure they've all been opened. The repeat the operation while profiling and view profile.log:

    :silent argdo e
    
    :profile start profile.log | profile func * | profile file *
    :silent argdo e
    :profile pause | noautocmd qall
    • Using BufExplorer 7.6.0, this results in:

      FUNCTIONS SORTED ON TOTAL TIME
      count     total (s)      self (s)  function
       1000   0.164370828   0.005045087  <SNR>2_ActivateBuffer()
       1000   0.130688003   0.006358159  <SNR>2_MRUPush()
       1000   0.112038402                <SNR>2_MRUPop()
       1000   0.029297798   0.025142784  <SNR>7_Highlight_Matching_Pair()
       1000   0.028637738   0.010886118  <SNR>2_UpdateTabBufData()
       1000   0.020331084                <SNR>8_LocalBrowse()
       1000   0.012291442                <SNR>2_ShouldIgnore()
       1000   0.011743064                <SNR>2_RemoveBufFromOtherTabs()
       1500   0.006926867                <SNR>7_Remove_Matches()
       1000   0.006008556                <SNR>2_AddBufToCurrentTab()
      
      FUNCTIONS SORTED ON SELF TIME
      count     total (s)      self (s)  function
       1000                 0.112038402  <SNR>2_MRUPop()
       1000   0.029297798   0.025142784  <SNR>7_Highlight_Matching_Pair()
       1000                 0.020331084  <SNR>8_LocalBrowse()
       1000                 0.012291442  <SNR>2_ShouldIgnore()
       1000                 0.011743064  <SNR>2_RemoveBufFromOtherTabs()
       1000   0.028637738   0.010886118  <SNR>2_UpdateTabBufData()
       1500                 0.006926867  <SNR>7_Remove_Matches()
       1000   0.130688003   0.006358159  <SNR>2_MRUPush()
       1000                 0.006008556  <SNR>2_AddBufToCurrentTab()
       1000   0.164370828   0.005045087  <SNR>2_ActivateBuffer()
      
    • Using the mru branch, this results in:

      FUNCTIONS SORTED ON TOTAL TIME
      count     total (s)      self (s)  function
       1000   0.081197834   0.008623676  <SNR>2_DoBufEnter()
       1000   0.062542936   0.014577688  <SNR>2_MRUAddBufTab()
       3000   0.035414244   0.024626039  <SNR>2_MRUAdd()
       1000   0.027307881   0.023379796  <SNR>7_Highlight_Matching_Pair()
       1000   0.017889854                <SNR>8_LocalBrowse()
       1000   0.012551004                <SNR>2_ShouldIgnore()
       1000   0.010788205   0.003414017  <SNR>2_MRURemove()
       1000   0.010031222   0.007982058  <SNR>2_MRUEnsureTabId()
       1000   0.007374188                <SNR>2_MRURemoveMustExist()
       1500   0.006580612                <SNR>7_Remove_Matches()
       1000   0.002049164                <SNR>2_GetTabId()
      
      FUNCTIONS SORTED ON SELF TIME
      count     total (s)      self (s)  function
       3000   0.035414244   0.024626039  <SNR>2_MRUAdd()
       1000   0.027307881   0.023379796  <SNR>7_Highlight_Matching_Pair()
       1000                 0.017889854  <SNR>8_LocalBrowse()
       1000   0.062542936   0.014577688  <SNR>2_MRUAddBufTab()
       1000                 0.012551004  <SNR>2_ShouldIgnore()
       1000   0.081197834   0.008623676  <SNR>2_DoBufEnter()
       1000   0.010031222   0.007982058  <SNR>2_MRUEnsureTabId()
       1000                 0.007374188  <SNR>2_MRURemoveMustExist()
       1500                 0.006580612  <SNR>7_Remove_Matches()
       1000   0.010788205   0.003414017  <SNR>2_MRURemove()
       1000                 0.002049164  <SNR>2_GetTabId()
      

@drmikehenry
Copy link
Copy Markdown
Contributor Author

I added one more commit to remove the obsolete documentation for the B command. The command itself has been gone since 2017.

@drmikehenry
Copy link
Copy Markdown
Contributor Author

I just noticed that the final commit (which removed the documentation for B) failed to delete the third line of the help for B. I've replaced that commit with a correction.

@drmikehenry
Copy link
Copy Markdown
Contributor Author

@jlanzarotta I just realized I've been overlooking an aspect of the g:bufExplorerOnlyOneTab configuration variable. Because the B command (which toggles this variable) had been deleted and because the variable was undocumented, I'd been thinking the configurability provided by this variable had been intentionally disabled (though incompletely removed from the codebase).

But now I see that setting g:bufExplorerOnlyOneTab = 0 provides an additional flexibility to the user.

With BufExplorer 7.6.0, perform the following commands:

:edit file9.txt
:edit file1.txt
:tabedit file9.txt
:edit file2.txt
:tabedit file9.txt
:edit file3.txt

Launch BufExplorer with \be and press T to view only the buffers associated with the current tab. This shows file3.txt and file9.txt:

" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | One tab/buffer | Absolute Split path | Show terminal
"=
  4 %a    file3.txt ~/testing line 1
  1 #     file9.txt ~/testing line 1

Quit BufExplorer and switch to tab 2:

:tabnext 2

Launch BufExplorer with \be; this now shows only file2.txt; though file2.txt had been seen in tab 2, it was seen more recently in tab 3, and in the default mode BufExplorer assigns a buffer to its most recent tab only:

" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | One tab/buffer | Absolute Split path | Show terminal
"=
  3 %a    file2.txt ~/testing line 1

Now restart Vim and change BufExplorer's mode via:

let g:bufExplorerOnlyOneTab = 0

Repeating the above demo, tab 3 still contains file3.txt and file9.txt. But after switching to tab 2 and launching BufExplorer, we see that tab 2 contains file2.txt and file9.txt:

" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | Absolute Split path | Show terminal
"=
  3 %a    file2.txt ~/testing line 1
  1 #     file9.txt ~/testing line 1

This is the additional mode I'd overlooked. The user can elect to see all buffers ever encountered in a tab (via g:bufExplorerOnlyOneTab = 0) or only the buffers which were more recently seen in this tab than in any other tab (via g:bufExplorerOnlyOneTab = 1).

As this seems like useful behavior, I'll rework this pull request to retain g:bufExplorerOnlyOneTab and add back the behavior when this variable is zero.

I'm assuming this means I should also restore the B command that was removed in 2017 so the user may toggle this functionality. I'll also document the variable in bufexplorer.txt. I apologize for not noticing this earlier.

These can be used to view or modify script-local variables and invoke
script-local functions from outside the plugin.

Examples:

- Display the MRU data structure `s:bufMru`:

      :echo BufExplorer_eval('s:bufMru')

- Use `s:MRUGetItems()` to display buffer numbers from this structure in
  MRU-order:

      :echo BufExplorer_eval('s:MRUGetItems(s:bufMru,0)')

- Re-initialize `s:bufMru`:

      :call BufExplorer_execute('let s:bufMru = s:MRUNew(0)')
Commit 3c0b11f (2017-09-18) removed the
`B` key that permitted toggling `g:bufExplorerOnlyOneTab`.  Restore this
command to bring back this documented functionality.
`s:ToggleOnlyOneTab()` lacked the optional parameter to
`s:RebuildBufferList()` required to remove excess lines when the number
of displayed buffers is expected to decrease.  Rather than add this
logic, remove the optional argument from `s:RebuildBufferList()` and
adjust `s:BuildBufferList()` to remove any excess lines in the event
that the line count is too large.  This unburdens callers of
`s:RebuildBufferList()` from the responsibility of determining how any
changes they've made will impact the number of lines in the buffer list.
This provides a way to track the identity of a tab as tabs are created
and deleted (which can cause tab numbers to change).
MRU tracking is now done dynamically as buffers come and go, so there is
no need to track buffers at plugin startup or session-load time.

- Issue jlanzarotta#8 "Bufexplorer MRU sort doesn't work properly if using
  restore_session.vim" was caused by resetting the MRU list via
  `s:Reset()` after a session is loaded.  Removing the `SessionLoadPost`
  autocmd that invokes this function resolves this issue.

- Issue jlanzarotta#87 "Bufexplorer interferes with the startinsert! command" was
  caused, as correctly diagnosed by the original poster, by the use of
  `:normal!` in the `s:CatalogBuffers()` function:

  ```vim
  silent execute 'normal! ' . tab . 'gt'
  ```

  `s:CatalogBuffers()` is no longer called at startup, resolving this
  issue.
Buffer-tracking auto-commands need not be delayed until the `VimEnter`
event.
This cleans up excess accumulation of deleted tabs and buffers in the
MRU data structures.  Tab deletions are expensive to track via
`TabClosed` events because those events fire after the tab has already
been deleted, so cleanup of dead tabs is handled via garbage collection.

Buffer deletions are tracked via `BufDelete` events, so garbage won't
build up in typical operation; but autocommands may be suppressed (e.g.,
via `:noautocmd bdelete`), necessitating garbage collection for buffers
as well.

To prevent excess garbage build-up caused by many tab deletions between
invocations of BufExplorer, tab garbage collection is invoked on
`TabClosed` events, for Vim versions where that event is supported.
@jlanzarotta
Copy link
Copy Markdown
Owner

Thanks for the update. I will review and merge in the next day or so.

@jlanzarotta
Copy link
Copy Markdown
Owner

jlanzarotta commented Mar 4, 2025

@jlanzarotta I just realized I've been overlooking an aspect of the g:bufExplorerOnlyOneTab configuration variable. Because the B command (which toggles this variable) had been deleted and because the variable was undocumented, I'd been thinking the configurability provided by this variable had been intentionally disabled (though incompletely removed from the codebase).

But now I see that setting g:bufExplorerOnlyOneTab = 0 provides an additional flexibility to the user.

With BufExplorer 7.6.0, perform the following commands:

:edit file9.txt
:edit file1.txt
:tabedit file9.txt
:edit file2.txt
:tabedit file9.txt
:edit file3.txt

Launch BufExplorer with \be and press T to view only the buffers associated with the current tab. This shows file3.txt and file9.txt:

" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | One tab/buffer | Absolute Split path | Show terminal
"=
  4 %a    file3.txt ~/testing line 1
  1 #     file9.txt ~/testing line 1

Quit BufExplorer and switch to tab 2:

:tabnext 2

Launch BufExplorer with \be; this now shows only file2.txt; though file2.txt had been seen in tab 2, it was seen more recently in tab 3, and in the default mode BufExplorer assigns a buffer to its most recent tab only:

" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | One tab/buffer | Absolute Split path | Show terminal
"=
  3 %a    file2.txt ~/testing line 1

Now restart Vim and change BufExplorer's mode via:

let g:bufExplorerOnlyOneTab = 0

Repeating the above demo, tab 3 still contains file3.txt and file9.txt. But after switching to tab 2 and launching BufExplorer, we see that tab 2 contains file2.txt and file9.txt:

" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | Absolute Split path | Show terminal
"=
  3 %a    file2.txt ~/testing line 1
  1 #     file9.txt ~/testing line 1

This is the additional mode I'd overlooked. The user can elect to see all buffers ever encountered in a tab (via g:bufExplorerOnlyOneTab = 0) or only the buffers which were more recently seen in this tab than in any other tab (via g:bufExplorerOnlyOneTab = 1).

As this seems like useful behavior, I'll rework this pull request to retain g:bufExplorerOnlyOneTab and add back the behavior when this variable is zero.

I'm assuming this means I should also restore the B command that was removed in 2017 so the user may toggle this functionality. I'll also document the variable in bufexplorer.txt. I apologize for not noticing this earlier.

Yes, I believe the g:bufExplorerOnlyOneTab code needs to be restored so functionality is not lost.

@jlanzarotta jlanzarotta closed this Mar 4, 2025
@jlanzarotta jlanzarotta reopened this Mar 4, 2025
In most cases, BufExplorer's own buffer won't be part of the list of
buffers because the buffer list is enumerated before creating the
`[BufExplorer]` buffer.  However, there is an edge case that occurs when
the current buffer is an empty unnamed buffer at BufExplorer launch.

When Vim starts, an empty unnamed buffer is active, e.g.:

```
vim
:ls
  1 %a   "[No Name]"                    line 1
```

Normally, editing a file via `:edit somefile.txt` would allocate a new
buffer number; but if the current buffer is empty and unnamed, Vim
reuses this buffer instead of allocating a new buffer, e.g.:

```
:edit somefile.txt
:ls
  1 %a   "somefile.txt"                 line 1
```

Notice that buffer 1 has been reused as `somefile.txt`.

This same thing happens when launching BufExplorer with an empty unnamed
buffer active.  With BufExplorer 7.6.0, launch Vim and immediately
invoke BufExplorer via `\be`; this results in:

```
" Press <F1> for Help
" Sorted by mru | Locate buffer | One tab/buffer | Absolute Split path | Show terminal
"=
  1 %a    [BufExplorer] ~/testing line 1
```

At BufExplorer startup, the buffer list is scanned to acquire buffer
numbers and attributes.  For performance reasons, the other buffer
metadata is gathered at display time.  Because the `[BufExplorer]`
buffer has taken over buffer 1 by that point, it shows up in the buffer
list with that name.

In prior BufExplorer versions, the problem presented in a different
fashion.  Using BufExplorer 7.5.0 or older, launch Vim and enable
BufExplorer display of unnamed buffers via:

```
:let g:bufExplorerShowNoName = 1
```

Now launch BufExplorer via `\be`:

```
" Press <F1> for Help
" Sorted by mru | Locate buffer | One tab/buffer | Absolute Split path | Show terminal
"=
  1 %a    [No Name] ~/testing                                                       line 1
```

Buffer 1 appears to be unnamed; but in actuality, it's really the
BufExplorer buffer (which is unlisted):

```
:ls!
  1u%a-  "[BufExplorer]"                line 4
```

The fix is to record the number of BufExplorer's buffer at startup and
prevent its display.
@drmikehenry
Copy link
Copy Markdown
Contributor Author

Update

I've updated the mru branch as explained above:

  • g:bufExplorerOnlyOneTab has been retained.

  • The new MRU algorithm honors g:bufExplorerOnlyOneTab.

  • The B command that was previously removed has been restored. B now toggles g:bufExplorerOnlyOneTab, changing between showing all buffers used on the current tab and showing buffers only on their MRU tab.

  • The documentation has been updated to cover g:bufExplorerOnlyOneTab and to clarify the <F1> help text for the B and T commands.

For clarity, I've updated the pull request text with the changes and have included some additional information, consolidated below.

I've also included a fix for a historical corner case that would allow BufExplorer's buffer to be incorrectly displayed.

Overview

This pull request provides a new MRU buffer tracking algorithm to:

  • Fix issue Bufexplorer MRU sort doesn't work properly if using restore_session.vim #8 "Bufexplorer MRU sort doesn't work properly if using restore_session.vim".

  • Fix issue Bufexplorer interferes with the startinsert! command #87 "Bufexplorer interferes with the startinsert! command".

  • Restore the B command that was previously removed, document the associated variable g:bufExplorerOnlyOneTab, and clarify the <F1> help text for the B and T commands.

  • Treat unlisted buffers as least-recently-used instead of most-recently-used in the MRU sort order.

  • Retain MRU tracking information independently from BufExplorer display modes. This allows changing display modes at any time without losing collected information. Track the set of all tabs where a buffer was used instead of just the most recently used tab; this allows the MRU tab to be closed without losing information about the next-most-recently-used tabs.

  • Do not display BufExplorer's buffer in the buffer listing.

  • Replace the undocumented m key (intended for debugging MRU logic) with the BufExplorer_eval() and BufExplorer_execute() general debugging features.

  • Improve speed of MRU tracking.

  • Gather together the logic for clearing excess lines in the BufExplorer listing.

Details

For demonstration purposes, create a temporary directory for testing and create a few test files:

mkdir testing
cd testing
touch file{1..500}.txt
printf '%s:1:1\n' file{1..500}.txt > 500files.out
printf '%s:1:1\n' file{1..9}.txt > 9files.out

500files.out and 9files.out have simulated compiler output suitable for use with :cgetfile; for example, 9files.out is:

file1.txt:1:1
file2.txt:1:1
file3.txt:1:1
file4.txt:1:1
file5.txt:1:1
file6.txt:1:1
file7.txt:1:1
file8.txt:1:1
file9.txt:1:1

MRU tracking on-demand

MRU tracking is now done only on-demand as buffers come and go, so there is no need to track buffers at plugin startup or session-load time. This resolves two issues:

  • Issue Bufexplorer MRU sort doesn't work properly if using restore_session.vim #8 "Bufexplorer MRU sort doesn't work properly if using restore_session.vim" was caused by resetting the MRU list via s:Reset() after a session is loaded. Removing the SessionLoadPost autocmd that invokes this function resolves this issue.

  • Issue Bufexplorer interferes with the startinsert! command #87 "Bufexplorer interferes with the startinsert! command" was caused, as correctly diagnosed by the original poster, by the use of :normal! in the s:CatalogBuffers() function:

    silent execute 'normal! ' . tab . 'gt'

    s:CatalogBuffers() is no longer called at startup, resolving this issue.

B command has been restored

The B command that was previously removed has been restored. B now toggles g:bufExplorerOnlyOneTab, changing between showing all buffers used on the current tab and showing buffers only on their MRU tab.

The documentation has been updated to cover g:bufExplorerOnlyOneTab and to clarify the <F1> help text for the B and T commands.

To demonstrate, launch Vim using the mru branch of BufExplorer, then run the following:

:edit file9.txt
:edit file1.txt
:tabedit file9.txt
:edit file2.txt

Then launch BufExplorer via \be and press T to show buffers only on the current tab. This results in:

 file1.txt  [BufExplorer]
" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | One tab/buffer | Absolute Split path | Show terminal
"=
  3 %a    file2.txt ~/testing line 1
  1 #     file9.txt ~/testing line 1

Quit BufExplorer via q and switch to tab 1:

:tabnext 1

Now launch BufExplorer via \be. This results in:

 [BufExplorer]  file2.txt
" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | One tab/buffer | Absolute Split path | Show terminal
"=
  2 %a    file1.txt ~/testing line 1

Though file9.txt was been opened in tab 1, it was subsequently opened in tab 2, making tab 2 the MRU tab for this buffer.

Now press B to toggle from showing One tab/buffer to showing all buffers associated with the tab. This results in:

 [BufExplorer]  file2.txt
" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | Absolute Split path | Show terminal
"=
  2 %a    file1.txt ~/testing line 1
  1 #     file9.txt ~/testing line 1

Note that file9.txt is now shown.

Unlisted buffers are least-recently-used

Unlisted buffers (as created by :grep or :cgetfile, for example) are now treated as least-recently-used instead of most-recently-used in the MRU sort order. For example, launch Vim and run these commands:

:edit file1.txt
:edit file2.txt
:cgetfile 9files.out

Then launch BufExplorer and press u to view unlisted buffers.

  • Using BufExplorer 7.6.0, this results in:

    " Press <F1> for Help
    " Sorted by mru | Locate buffer | Show unlisted | One tab/buffer | Absolute Split path | Show terminal
    "=
      2 %a    file2.txt ~/testing line 1
      3u      file3.txt ~/testing line 0
      4u      file4.txt ~/testing line 0
      5u      file5.txt ~/testing line 0
      6u      file6.txt ~/testing line 0
      7u      file7.txt ~/testing line 0
      8u      file8.txt ~/testing line 0
      9u      file9.txt ~/testing line 0
      1 #     file1.txt ~/testing line 1
    

    Note that the unlisted buffers 3-9 (which have never been opened) are found between buffers 1 and 2 (which were used most recently).

  • Using the mru branch, unlisted buffers are shown last:

    " Press <F1> for Help
    " Sorted by mru | Locate buffer | Show unlisted | Absolute Split path | Show terminal
    "=
      2 %a    file2.txt ~/testing line 1
      1 #     file1.txt ~/testing line 1
      3u      file3.txt ~/testing line 0
      4u      file4.txt ~/testing line 0
      5u      file5.txt ~/testing line 0
      6u      file6.txt ~/testing line 0
      7u      file7.txt ~/testing line 0
      8u      file8.txt ~/testing line 0
      9u      file9.txt ~/testing line 0
    

MRU tracking is independent of display

MRU tracking information is now collected independently of BufExplorer display modes. When using B to toggle between showing all buffers used on the current tab and only those buffers whose MRU tab is the current tab, no MRU tracking information is changed. For example, launch Vim and run these commands:

:edit file9.txt
:edit file1.txt
:tabedit file9.txt
:edit file2.txt
:tabedit file9.txt
:edit file3.txt

Then launch BufExplorer via \be and press T to show buffers only on the current tab. This results in:

 file1.txt  file2.txt  [BufExplorer]
" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | One tab/buffer | Absolute Split path | Show terminal
"=
  4 %a    file3.txt ~/testing line 1
  1 #     file9.txt ~/testing line 1

Quit BufExplorer via q and switch to tab 2:

:tabnext 2

Now launch BufExplorer via \be. This results in:

 file1.txt  [BufExplorer]  file3.txt
" Press <F1> for Help
" Sorted by mru | Locate buffer | Show buffers/tab | One tab/buffer | Absolute Split path | Show terminal
"=
  3 %a    file2.txt ~/testing line 1

Though file9.txt had been opened in tab 2, it was subsequently opened in tab 3, making tab 3 the MRU tab for this buffer.

Now quit BufExplorer with q, then close tab 3:

:tabclose 3

Because tab 3 no longer exists, tab 2 should be the new MRU tab for file9.txt. Launch BufExplorer with \be.

  • Using BufExplorer 7.6.0, this results in:

     file1.txt  [BufExplorer]
    " Press <F1> for Help
    " Sorted by mru | Locate buffer | Show buffers/tab | One tab/buffer | Absolute Split path | Show terminal
    "=
      3 %a    file2.txt ~/testing line 1
    

    Note that file9.txt should be present, but it is not shown because it was deleted from tab 2's metadata when it was shown in tab 3.

  • Using the mru branch, this results in:

     file1.txt  [BufExplorer]
    " Press <F1> for Help
    " Sorted by mru | Locate buffer | Show buffers/tab | One tab/buffer | Absolute Split path | Show terminal
    "=
      3 %a    file2.txt ~/testing line 1
      1       file9.txt ~/testing line 1
    

    Note that file9.txt is present because the metadata for tab 2 was retained independently of the display mode.

Do not display BufExplorer's buffer in the buffer listing

In most cases, BufExplorer's own buffer won't be part of the list of buffers because the buffer list is enumerated before creating the [BufExplorer] buffer. However, there is an edge case that occurs when the current buffer is an empty unnamed buffer at BufExplorer launch (via \be).

When Vim starts, an empty unnamed buffer is active, e.g.:

vim
:ls
  1 %a   "[No Name]"                    line 1

Normally, editing a file via :edit somefile.txt would allocate a new buffer number; but if the current buffer is empty and unnamed, Vim reuses this buffer instead of allocating a new buffer, e.g.:

:edit somefile.txt
:ls
  1 %a   "somefile.txt"                 line 1

Notice that buffer 1 has been reused as somefile.txt.

This same thing happens when launching BufExplorer with an empty unnamed buffer active. With BufExplorer 7.6.0, launch Vim and immediately invoke BufExplorer via \be; this results in:

" Press <F1> for Help
" Sorted by mru | Locate buffer | One tab/buffer | Absolute Split path | Show terminal
"=
  1 %a    [BufExplorer] ~/testing line 1

At BufExplorer startup, the buffer list is scanned to acquire buffer numbers and attributes. For performance reasons, the other buffer metadata is gathered at display time. Because the [BufExplorer] buffer has taken over buffer 1 by that point, it shows up in the buffer list with that name.

In prior BufExplorer versions, the problem presented in a different fashion. Using BufExplorer 7.5.0 or older, launch Vim and enable BufExplorer display of unnamed buffers via:

:let g:bufExplorerShowNoName = 1

Now launch BufExplorer via \be:

" Press <F1> for Help
" Sorted by mru | Locate buffer | One tab/buffer | Absolute Split path | Show terminal
"=
  1 %a    [No Name] ~/testing                                                       line 1

Buffer 1 appears to be unnamed; but in actuality, it's really the BufExplorer buffer (which is unlisted):

:ls!
  1u%a-  "[BufExplorer]"                line 4

The fix is to record the number of BufExplorer's buffer at startup and prevent its display.

Undocumented m key has been removed

The undocumented m key (intended for debugging MRU logic) has been removed and replace with more general BufExplorer_eval() and BufExplorer_execute() debugging features. These can be used to view or modify script-local variables and invoke script-local functions from outside the plugin. For example:

  • To display the MRU data structure s:bufMru:

    :echo BufExplorer_eval('s:bufMru')
    
  • To use s:MRUGetItems() to display buffer numbers from this structure in MRU-order:

    :echo BufExplorer_eval('s:MRUGetItems(s:bufMru,0)')
    
  • To re-initialize s:bufMru:

    :call BufExplorer_execute('let s:bufMru = s:MRUNew(0)')
    

MRU algorithm speed improvement

The new MRU algorithm gathers buffer MRU data using a doubly linked list to avoid linear searching through arrays, and it defers display-related computations until BufExplorer has been invoked, improving MRU data collection speed. To demonstrate, launch Vim via:

vim file*.txt

First, visit all files in the argument list to ensure they've all been opened. The repeat the operation while profiling and view profile.log:

:silent argdo e

:profile start profile.log | profile func * | profile file *
:silent argdo e
:profile pause | noautocmd qall
  • Using BufExplorer 7.6.0, this results in:

    FUNCTIONS SORTED ON TOTAL TIME
    count     total (s)      self (s)  function
     1000   0.154888428   0.004741391  <SNR>2_ActivateBuffer()
     1000   0.123024045   0.005978059  <SNR>2_MRUPush()
     1000   0.105447741                <SNR>2_MRUPop()
     1000   0.027217444   0.023349485  <SNR>7_Highlight_Matching_Pair()
     1000   0.027122992   0.010361974  <SNR>2_UpdateTabBufData()
     1000   0.019216996                <SNR>8_LocalBrowse()
     1000   0.011598245                <SNR>2_ShouldIgnore()
     1000   0.011180042                <SNR>2_RemoveBufFromOtherTabs()
     1500   0.006584432                <SNR>7_Remove_Matches()
     1000   0.005580976                <SNR>2_AddBufToCurrentTab()
    
    FUNCTIONS SORTED ON SELF TIME
    count     total (s)      self (s)  function
     1000                 0.105447741  <SNR>2_MRUPop()
     1000   0.027217444   0.023349485  <SNR>7_Highlight_Matching_Pair()
     1000                 0.019216996  <SNR>8_LocalBrowse()
     1000                 0.011598245  <SNR>2_ShouldIgnore()
     1000                 0.011180042  <SNR>2_RemoveBufFromOtherTabs()
     1000   0.027122992   0.010361974  <SNR>2_UpdateTabBufData()
     1500                 0.006584432  <SNR>7_Remove_Matches()
     1000   0.123024045   0.005978059  <SNR>2_MRUPush()
     1000                 0.005580976  <SNR>2_AddBufToCurrentTab()
     1000   0.154888428   0.004741391  <SNR>2_ActivateBuffer()
    
  • Using the mru branch, this results in:

    FUNCTIONS SORTED ON TOTAL TIME
    count     total (s)      self (s)  function
     1000   0.079911819   0.008392547  <SNR>2_DoBufEnter()
     1000   0.061751151   0.014291655  <SNR>2_MRUAddBufTab()
     3000   0.035130779   0.024450465  <SNR>2_MRUAdd()
     1000   0.027804189   0.023804231  <SNR>7_Highlight_Matching_Pair()
     1000   0.017749359                <SNR>8_LocalBrowse()
     1000   0.012328717                <SNR>2_ShouldIgnore()
     1000   0.010680314   0.003417594  <SNR>2_MRURemove()
     1000   0.009768121   0.007812607  <SNR>2_MRUEnsureTabId()
     1000   0.007262720                <SNR>2_MRURemoveMustExist()
     1500   0.006683859                <SNR>7_Remove_Matches()
     1000   0.001955514                <SNR>2_GetTabId()
    
    FUNCTIONS SORTED ON SELF TIME
    count     total (s)      self (s)  function
     3000   0.035130779   0.024450465  <SNR>2_MRUAdd()
     1000   0.027804189   0.023804231  <SNR>7_Highlight_Matching_Pair()
     1000                 0.017749359  <SNR>8_LocalBrowse()
     1000   0.061751151   0.014291655  <SNR>2_MRUAddBufTab()
     1000                 0.012328717  <SNR>2_ShouldIgnore()
     1000   0.079911819   0.008392547  <SNR>2_DoBufEnter()
     1000   0.009768121   0.007812607  <SNR>2_MRUEnsureTabId()
     1000                 0.007262720  <SNR>2_MRURemoveMustExist()
     1500                 0.006683859  <SNR>7_Remove_Matches()
     1000   0.010680314   0.003417594  <SNR>2_MRURemove()
     1000                 0.001955514  <SNR>2_GetTabId()
    

Gather logic to clear excess lines into s:BuildBufferList()

This is a code consolidation as a side effect of noticing that the newly restored s:ToggleOnlyOneTab() function was not asking s:RebuildBufferList() to clear excess lines. s:ToggleOnlyOneTab() lacked the optional parameter to s:RebuildBufferList() required to remove excess lines when the number of displayed buffers is expected to decrease. Rather than add this logic, the optional argument was removed from s:RebuildBufferList() and s:BuildBufferList() was adjusted to remove any excess lines in the event that the line count is too large. This unburdens callers of s:RebuildBufferList() from the responsibility of determining how any changes they've made will impact the number of lines in the buffer list.

@jlanzarotta
Copy link
Copy Markdown
Owner

Fantastic! Let me check the changes out again and give them a test.

@jlanzarotta jlanzarotta merged commit 658b7b0 into jlanzarotta:master Mar 5, 2025
@drmikehenry drmikehenry deleted the mru branch March 16, 2025 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants