Просмотр исходного кода

Merge pull request #121 from drmikehenry/mru

Improve buffer MRU tracking algorithm
jlanzarotta 10 месяцев назад
Родитель
Сommit
658b7b00aa
2 измененных файлов с 386 добавлено и 181 удалено
  1. 11 0
      doc/bufexplorer.txt
  2. 375 181
      plugin/bufexplorer.vim

+ 11 - 0
doc/bufexplorer.txt

@@ -173,6 +173,17 @@ The default is to be taken to the active window.
 When a buffer is selected, the functions specified either singly or as a list
 will be called.
 
+                                                      *g:bufExplorerOnlyOneTab*
+To control whether a buffer should be shown on any tab where it has ever been
+used or only on the most-recently-used (MRU) tab (i.e., the tab where the buffer
+was most recently used), use: >
+  let g:bufExplorerOnlyOneTab=0        " Show in all tabs where buffer was used.
+  let g:bufExplorerOnlyOneTab=1        " Show buffer only in MRU tab.
+The default is `1` (show a buffer only on the MRU tab).
+This setting applies only when |g:bufExplorerShowTabBuffer| is true (i.e., when
+BufExplorer shows only those buffers used in the current tab instead of showing
+all buffers).
+
                                                      *g:bufExplorerReverseSort*
 To control whether to sort the buffer in reverse order or not, use: >
   let g:bufExplorerReverseSort=0       " Do not sort in reverse order.

+ 375 - 181
plugin/bufexplorer.vim

@@ -127,187 +127,366 @@ endfunction
 
 " Script variables {{{2
 let s:MRU_Exclude_List = ["[BufExplorer]","__MRU_Files__","[Buf\ List]"]
-let s:MRUList = []
 let s:name = '[BufExplorer]'
 let s:originBuffer = 0
+" Buffer number of the BufExplorer window.
+let s:bufExplorerBuffer = 0
 let s:running = 0
 let s:sort_by = ["number", "name", "fullpath", "mru", "extension"]
 let s:splitMode = ""
 let s:didSplit = 0
 let s:types = ["fullname", "homename", "path", "relativename", "relativepath", "shortname"]
 
-" Setup the autocommands that handle the MRUList and other stuff. {{{2
-autocmd VimEnter * call s:Setup()
-
-" Reset MRUList and buffer->tab associations after loading a session. {{{2
-autocmd SessionLoadPost * call s:Reset()
-
-" Setup {{{2
-function! s:Setup()
-    call s:Reset()
-
-    " Now that the MRUList is created, add the other autocmds.
-    augroup BufExplorer
-        autocmd!
-        autocmd BufEnter,BufNew * call s:ActivateBuffer()
-        autocmd BufWipeOut * call s:DeactivateBuffer(1)
-        autocmd BufDelete * call s:DeactivateBuffer(0)
-        autocmd BufWinEnter \[BufExplorer\] call s:Initialize()
-        autocmd BufWinLeave \[BufExplorer\] call s:Cleanup()
-    augroup END
-endfunction
-
-" Reset {{{2
-function! s:Reset()
-    " Build initial MRUList. This makes sure all the files specified on the
-    " command line are picked up correctly. Check buffers exist so this also
-    " works after wiping buffers and loading a session (e.g. sessionman.vim)
-    let s:MRUList = filter(range(1, bufnr('$')), 'bufexists(v:val)')
-
-    " Initialize the association of buffers to tabs for any buffers
-    " that have been created prior to now, e.g., files specified as
-    " vim command line arguments
-    call s:CatalogBuffers()
-endfunction
-
-" CatalogBuffers {{{2
-" Create tab associations for any existing buffers
-function! s:CatalogBuffers()
-    let ct = tabpagenr()
-
-    for tab in range(1, tabpagenr('$'))
-        silent execute 'normal! ' . tab . 'gt'
-        for buf in tabpagebuflist()
-            call s:UpdateTabBufData(buf)
-        endfor
-    endfor
-
-    silent execute 'normal! ' . ct . 'gt'
+" Setup the autocommands that handle stuff. {{{2
+augroup BufExplorer
+    autocmd!
+    autocmd WinEnter        * call s:DoWinEnter()
+    autocmd BufEnter        * call s:DoBufEnter()
+    autocmd BufDelete       * call s:DoBufDelete()
+    if exists('##TabClosed')
+        autocmd TabClosed      * call s:DoTabClosed()
+    endif
+    autocmd BufWinEnter \[BufExplorer\] call s:Initialize()
+    autocmd BufWinLeave \[BufExplorer\] call s:Cleanup()
+augroup END
+
+" AssignTabId {{{2
+" Assign a `tabId` to the given tab.
+function! s:AssignTabId(tabNbr)
+    " Create a unique `tabId` based on the current time and an incrementing
+    " counter value that helps ensure uniqueness.
+    let tabId = reltimestr(reltime()) . ':' . s:tabIdCounter
+    call settabvar(a:tabNbr, 'bufexp_tabId', tabId)
+    let s:tabIdCounter = (s:tabIdCounter + 1) % 1000000000
+    return tabId
+endfunction
+
+let s:tabIdCounter = 0
+
+" GetTabId {{{2
+" Retrieve the `tabId` for the given tab (or '' if the tab has no `tabId`).
+function! s:GetTabId(tabNbr)
+    return gettabvar(a:tabNbr, 'bufexp_tabId', '')
+endfunction
+
+" MRU data structure {{{2
+" An MRU data structure is a dictionary that holds a circular doubly linked list
+" of `item` values.  The dictionary contains three keys:
+"   'head': a sentinel `item` representing the head of the list.
+"   'next': a dictionary mapping an `item` to the next `item` in the list.
+"   'prev': a dictionary mapping an `item` to the previous `item` in the list.
+" E.g., an MRU holding buffer numbers will use `0` (an invalid buffer number) as
+" `head`.  With the buffer numbers `1`, `2`, and `3`, an example MRU would be:
+"
+"           +--<---------<---------<---------<---------<+
+"  `next`   |                                           |
+"           +--> +---+ --> +---+ --> +---+ --> +---+ -->+
+"  `head`        | 0 |     | 1 |     | 2 |     | 3 |
+"           +<-- +---+ <-- +---+ <-- +---+ <-- +---+ <--+
+"  `prev`   |                                           |
+"           +->-------->--------->--------->--------->--+
+"
+" `head` allows the chosen sentinel item to differ in value and type; for
+" example, `head` could be the string '.', allowing an MRU of strings (such as
+" for `TabId` values).
+"
+" Note that dictionary keys are always strings.  Integers may be used, but they
+" are converted to strings when used (and `keys(theDictionary)` will be a
+" list of strings, not of integers).
+
+" MRUNew {{{2
+function! s:MRUNew(head)
+    let [next, prev] = [{}, {}]
+    let next[a:head] = a:head
+    let prev[a:head] = a:head
+    return { 'head': a:head, 'next': next, 'prev': prev }
+endfunction
+
+" MRULen {{{2
+function! s:MRULen(mru)
+    " Do not include the always-present `mru.head` item.
+    return len(a:mru.next) - 1
+endfunction
+
+" MRURemoveMustExist {{{2
+"   `item` must exist in `mru`.
+function! s:MRURemoveMustExist(mru, item)
+    let [next, prev] = [a:mru.next, a:mru.prev]
+    let prevItem = prev[a:item]
+    let nextItem = next[a:item]
+    let next[prevItem] = nextItem
+    let prev[nextItem] = prevItem
+    unlet next[a:item]
+    unlet prev[a:item]
+endfunction
+
+" MRURemove {{{2
+"   `item` need not exist in `mru`.
+function! s:MRURemove(mru, item)
+    if has_key(a:mru.next, a:item)
+        call s:MRURemoveMustExist(a:mru, a:item)
+    endif
 endfunction
 
-" AssociatedTab {{{2
-" Return the number of the tab associated with the specified buffer. If the
-" buffer is associated with more than one tab, the first one found is
-" returned. If the buffer is not associated with any tabs, -1 is returned.
-function! s:AssociatedTab(bufnr)
-    for tab in range(1, tabpagenr('$'))
-        let list = gettabvar(tab, 'bufexp_buf_list', [])
-        let idx = index(list, a:bufnr)
-        if idx != -1
-            return tab
-        endif
-    endfor
-
-    return -1
+" MRUAdd {{{2
+function! s:MRUAdd(mru, item)
+    let [next, prev] = [a:mru.next, a:mru.prev]
+    let prevItem = a:mru.head
+    let nextItem = next[prevItem]
+    if a:item != nextItem
+        call s:MRURemove(a:mru, a:item)
+        let next[a:item] = nextItem
+        let prev[a:item] = prevItem
+        let next[prevItem] = a:item
+        let prev[nextItem] = a:item
+    endif
 endfunction
 
-" RemoveBufFromOtherTabs {{{2
-" Remove the specified buffer from the buffer lists of all tabs except the
-" current tab.
-function! s:RemoveBufFromOtherTabs(bufnr)
-    for tab in range(1, tabpagenr('$'))
-        if tab == tabpagenr()
-            continue
+" MRUGetItems {{{2
+"   Return list of up to `maxItems` items in MRU order.
+"   `maxItems == 0` => unlimited.
+function! s:MRUGetItems(mru, maxItems)
+    let [head, next] = [a:mru.head, a:mru.next]
+    let items = []
+    let item = next[head]
+    while item != head
+        if a:maxItems > 0 && len(items) >= a:maxItems
+            break
         endif
-
-        let list = gettabvar(tab, 'bufexp_buf_list', [])
-        let idx = index(list, a:bufnr)
-        if idx == -1
-            continue
+        call add(items, item)
+        let item = next[item]
+    endwhile
+    return items
+endfunction
+
+" MRUGetOrdering {{{2
+"   Return dictionary mapping up to `maxItems` from `item` to MRU order.
+"   `maxItems == 0` => unlimited.
+function! s:MRUGetOrdering(mru, maxItems)
+    let [head, next] = [a:mru.head, a:mru.next]
+    let items = {}
+    let order = 0
+    let item = next[head]
+    while item != head
+        if a:maxItems > 0 && order >= a:maxItems
+            break
         endif
+        let items[item] = order
+        let order = order + 1
+        let item = next[item]
+    endwhile
+    return items
+endfunction
+
+" MRU trackers {{{2
+" `.head` value for tab MRU:
+let s:tabIdHead = '.'
+
+" Track MRU buffers globally (independent of tabs).
+let s:bufMru = s:MRUNew(0)
+
+" Track MRU buffers for each tab, indexed by `tabId`.
+"   `s:bufMruByTab[tabId] -> MRU structure`.
+let s:bufMruByTab = {}
+
+" Track MRU tabs for each buffer, indexed by `bufNbr`.
+"   `s:tabMruByBuf[burNbr] -> MRU structure`.
+let s:tabMruByBuf = {}
+
+" MRURemoveBuf {{{2
+function! s:MRURemoveBuf(bufNbr)
+    call s:MRURemove(s:bufMru, a:bufNbr)
+    if has_key(s:tabMruByBuf, a:bufNbr)
+        let mru = s:tabMruByBuf[a:bufNbr]
+        let [head, next] = [mru.head, mru.next]
+        let tabId = next[head]
+        while tabId != head
+            call s:MRURemoveMustExist(s:bufMruByTab[tabId], a:bufNbr)
+            let tabId = next[tabId]
+        endwhile
+        unlet s:tabMruByBuf[a:bufNbr]
+    endif
+endfunction
 
-        call remove(list, idx)
-        call settabvar(tab, 'bufexp_buf_list', list)
-    endfor
+" MRURemoveTab {{{2
+function! s:MRURemoveTab(tabId)
+    if has_key(s:bufMruByTab, a:tabId)
+        let mru = s:bufMruByTab[a:tabId]
+        let [head, next] = [mru.head, mru.next]
+        let bufNbr = next[head]
+        while bufNbr != head
+            call s:MRURemoveMustExist(s:tabMruByBuf[bufNbr], a:tabId)
+            let bufNbr = next[bufNbr]
+        endwhile
+        unlet s:bufMruByTab[a:tabId]
+    endif
 endfunction
 
-" AddBufToCurrentTab {{{2
-" Add the specified buffer to the list of buffers associated with the current
-" tab.
-function! s:AddBufToCurrentTab(bufnr)
-    if index(t:bufexp_buf_list, a:bufnr) == -1
-        call add(t:bufexp_buf_list, a:bufnr)
+" MRUAddBufTab {{{2
+function! s:MRUAddBufTab(bufNbr, tabId)
+    if s:ShouldIgnore(a:bufNbr)
+        return
+    endif
+    call s:MRUAdd(s:bufMru, a:bufNbr)
+    if !has_key(s:bufMruByTab, a:tabId)
+        let s:bufMruByTab[a:tabId] = s:MRUNew(0)
+    endif
+    let bufMru = s:bufMruByTab[a:tabId]
+    call s:MRUAdd(bufMru, a:bufNbr)
+    if !has_key(s:tabMruByBuf, a:bufNbr)
+        let s:tabMruByBuf[a:bufNbr] = s:MRUNew(s:tabIdHead)
     endif
+    let tabMru = s:tabMruByBuf[a:bufNbr]
+    call s:MRUAdd(tabMru, a:tabId)
+endfunction
+
+" MRUTabForBuf {{{2
+"   Return `tabId` most recently used by `bufNbr`.
+"   If no `tabId` is found for `bufNbr`, return `s:tabIdHead`.
+function! s:MRUTabForBuf(bufNbr)
+    let tabMru = get(s:tabMruByBuf, a:bufNbr, s:alwaysEmptyTabMru)
+    return tabMru.next[tabMru.head]
+endfunction
+
+" An always-empty MRU for tabs as a default when looking up
+" `s:tabMruByBuf[bufNbr]` for an unknown `bufNbr`.
+let s:alwaysEmptyTabMru = s:MRUNew(s:tabIdHead)
+
+" MRUTabHasSeenBuf {{{2
+"   Return true if `tabId` has ever seen `bufNbr`.
+function! s:MRUTabHasSeenBuf(tabId, bufNbr)
+    let mru = get(s:bufMruByTab, a:tabId, s:alwaysEmptyBufMru)
+    return has_key(mru.next, a:bufNbr)
 endfunction
 
-" IsInCurrentTab {{{2
-" Returns whether the specified buffer is associated with the current tab.
-function! s:IsInCurrentTab(bufnr)
-    " It shouldn't happen that the list of buffers is not defined but if it
-    " does, play it safe and include the buffer.
-    if !exists('t:bufexp_buf_list')
+" MRUTabShouldShowBuf {{{2
+"   Return true if `tabId` should show `bufNbr`.
+"   This is a function of current display modes.
+function! s:MRUTabShouldShowBuf(tabId, bufNbr)
+    if !g:bufExplorerShowTabBuffer
+        " We are showing buffers from all tabs.
         return 1
+    elseif g:bufExplorerOnlyOneTab
+        " We are showing buffers that were most recently seen in this tab.
+        return s:MRUTabForBuf(a:bufNbr) == a:tabId
+    else
+        " We are showing buffers that have ever been seen in this tab.
+        return s:MRUTabHasSeenBuf(a:tabId, a:bufNbr)
     endif
-
-    return (index(t:bufexp_buf_list, a:bufnr) != -1)
 endfunction
 
-" UpdateTabBufData {{{2
-" Update the tab buffer data for the specified buffer.
-"
-" The current tab's list is updated. If a buffer is only allowed to be
-" associated with one tab, it is removed  from the lists of any other tabs
-" with which it may have been associated.
-"
-" The associations between tabs and buffers are maintained in separate lists
-" for each tab, which are stored in tab-specific variables
-" 't:bufexp_buf_list'.
-function! s:UpdateTabBufData(bufnr)
-    " The first time we add a tab, Vim uses the current buffer as its starting
-    " page even though we are about to edit a new page, and another BufEnter
-    " for the new page is triggered later. Use this first BufEnter to
-    " initialize the list of buffers, but don't add the buffer number to the
-    " list if it is already associated with another tab.
-    "
-    " Unfortunately, this doesn't work right when the first buffer opened in
-    " the tab should be associated with it, such as when 'tab split +buffer N'
-    " is used.
-    if !exists("t:bufexp_buf_list")
-        let t:bufexp_buf_list = []
-
-        if s:AssociatedTab(a:bufnr) != -1
-            return
+" MRUListedBuffersForTab {{{2
+"   Return list of up to `maxBuffers` listed buffers in MRU order for the tab.
+"   `maxBuffers == 0` => unlimited.
+function! s:MRUListedBuffersForTab(tabId, maxBuffers)
+    let bufNbrs = []
+    let mru = get(s:bufMruByTab, a:tabId, s:alwaysEmptyBufMru)
+    let [head, next] = [mru.head, mru.next]
+    let bufNbr = next[head]
+    while bufNbr != head
+        if a:maxBuffers > 0 && len(bufNbrs) >= a:maxBuffers
+            break
+        endif
+        if buflisted(bufNbr) && s:MRUTabShouldShowBuf(a:tabId, bufNbr)
+            call add(bufNbrs, bufNbr)
+        endif
+        let bufNbr = next[bufNbr]
+    endwhile
+    return bufNbrs
+endfunction
+
+" An always-empty MRU for buffers as a default when looking up
+" `s:bufMruByTab[tabId]` for an unknown `tabId`.
+let s:alwaysEmptyBufMru = s:MRUNew(0)
+
+" MRUOrderForBuf {{{2
+" Return the position of `bufNbr` in the current MRU ordering.
+" This is a function of the current display mode.  When showing buffers from all
+" tabs, it's the global MRU order; otherwise, it the MRU order for the tab at
+" BufExplorer launch.  The latter includes all buffers seen in this tab, which
+" is sufficient whether `g:bufExplorerOnlyOneTab` is true or false.
+function! s:MRUOrderForBuf(bufNbr)
+    if !exists('s:mruOrder')
+        if g:bufExplorerShowTabBuffer
+            let mru = get(s:bufMruByTab, s:tabIdAtLaunch, s:alwaysEmptyBufMru)
+        else
+            let mru = s:bufMru
         endif
+        let s:mruOrder = s:MRUGetOrdering(mru, 0)
     endif
+    return get(s:mruOrder, a:bufNbr, len(s:mruOrder))
+endfunction
 
-    call s:AddBufToCurrentTab(a:bufnr)
-
-    if g:bufExplorerOnlyOneTab
-        call s:RemoveBufFromOtherTabs(a:bufnr)
+" MRUEnsureTabId {{{2
+function! s:MRUEnsureTabId(tabNbr)
+    let tabId = s:GetTabId(a:tabNbr)
+    if tabId == ''
+        let tabId = s:AssignTabId(a:tabNbr)
+        for bufNbr in tabpagebuflist(a:tabNbr)
+            call s:MRUAddBufTab(bufNbr, tabId)
+        endfor
     endif
+    return tabId
 endfunction
 
-" ActivateBuffer {{{2
-function! s:ActivateBuffer()
-    let _bufnr = bufnr("%")
-    call s:UpdateTabBufData(_bufnr)
-    call s:MRUPush(_bufnr)
+" MRUGarbageCollectBufs {{{2
+"   Requires `s:raw_buffer_listing`.
+function! s:MRUGarbageCollectBufs()
+    for bufNbr in values(s:bufMru.next)
+        if bufNbr != 0 && !has_key(s:raw_buffer_listing, bufNbr)
+            call s:MRURemoveBuf(bufNbr)
+        endif
+    endfor
 endfunction
 
-" DeactivateBuffer {{{2
-function! s:DeactivateBuffer(remove)
-    let _bufnr = str2nr(expand("<abuf>"))
-    call s:MRUPop(_bufnr)
+" MRUGarbageCollectTabs {{{2
+function! s:MRUGarbageCollectTabs()
+    let numTabs = tabpagenr('$')
+    let liveTabIds = {}
+    for tabNbr in range(1, numTabs)
+        let tabId = s:GetTabId(tabNbr)
+        if tabId != ''
+            let liveTabIds[tabId] = 1
+        endif
+    endfor
+    for tabId in keys(s:bufMruByTab)
+        if tabId != s:tabIdHead && !has_key(liveTabIds, tabId)
+            call s:MRURemoveTab(tabId)
+        endif
+    endfor
 endfunction
 
-" MRUPop {{{2
-function! s:MRUPop(bufnr)
-    call filter(s:MRUList, 'v:val != '.a:bufnr)
+" DoWinEnter {{{2
+function! s:DoWinEnter()
+    let bufNbr = str2nr(expand("<abuf>"))
+    let tabNbr = tabpagenr()
+    let tabId = s:GetTabId(tabNbr)
+    " Ignore `WinEnter` for a newly created tab; this event comes when creating
+    " a new tab, and the buffer at that moment is one that is about to be
+    " replaced by the buffer to which we are switching; this latter buffer will
+    " be handled by the forthcoming `BufEnter` event.
+    if tabId != ''
+        call s:MRUAddBufTab(bufNbr, tabId)
+    endif
 endfunction
 
-" MRUPush {{{2
-function! s:MRUPush(buf)
-    " Skip temporary buffer with buftype set. Don't add the BufExplorer window
-    " to the list.
-    if s:ShouldIgnore(a:buf) == 1
-        return
-    endif
+" DoBufEnter {{{2
+function! s:DoBufEnter()
+    let bufNbr = str2nr(expand("<abuf>"))
+    let tabNbr = tabpagenr()
+    let tabId = s:MRUEnsureTabId(tabNbr)
+    call s:MRUAddBufTab(bufNbr, tabId)
+endfunction
 
-    " Remove the buffer number from the list if it already exists.
-    call s:MRUPop(a:buf)
+" DoBufDelete {{{2
+function! s:DoBufDelete()
+    let bufNbr = str2nr(expand("<abuf>"))
+    call s:MRURemoveBuf(bufNbr)
+endfunction
 
-    " Add the buffer number to the head of the list.
-    call insert(s:MRUList, a:buf)
+" DoTabClosed {{{2
+function! s:DoTabClosed()
+    call s:MRUGarbageCollectTabs()
 endfunction
 
 " ShouldIgnore {{{2
@@ -434,9 +613,16 @@ function! BufExplorer()
 
     " Add zero to ensure the variable is treated as a number.
     let s:originBuffer = bufnr("%") + 0
+    let s:tabIdAtLaunch = s:MRUEnsureTabId(tabpagenr())
+
+    " Forget any cached MRU ordering from previous invocations.
+    unlet! s:mruOrder
 
     silent let s:raw_buffer_listing = s:GetBufferInfo(0)
 
+    call s:MRUGarbageCollectBufs()
+    call s:MRUGarbageCollectTabs()
+
     " We may have to split the current window.
     if s:splitMode != ""
         " Save off the original settings.
@@ -466,6 +652,9 @@ function! BufExplorer()
         execute "silent keepjumps hide edit".name
     endif
 
+    " Record BufExplorer's buffer number.
+    let s:bufExplorerBuffer = bufnr('%')
+
     call s:DisplayBufferList()
 
     " Position the cursor in the newly displayed list on the line representing
@@ -474,6 +663,9 @@ function! BufExplorer()
     execute search("%")
 endfunction
 
+" Tracks `tabId` at BufExplorer launch.
+let s:tabIdAtLaunch = ''
+
 " DisplayBufferList {{{2
 function! s:DisplayBufferList()
     setlocal buftype=nofile
@@ -514,13 +706,13 @@ function! s:MapKeys()
     nnoremap <script> <silent> <nowait> <buffer> <s-cr>        :call <SID>SelectBuffer("tab")<CR>
     nnoremap <script> <silent> <nowait> <buffer> a             :call <SID>ToggleFindActive()<CR>
     nnoremap <script> <silent> <nowait> <buffer> b             :call <SID>SelectBuffer("ask")<CR>
+    nnoremap <script> <silent> <nowait> <buffer> B             :call <SID>ToggleOnlyOneTab()<CR>
     nnoremap <script> <silent> <nowait> <buffer> d             :call <SID>RemoveBuffer("delete")<CR>
     xnoremap <script> <silent> <nowait> <buffer> d             :call <SID>RemoveBuffer("delete")<CR>
     nnoremap <script> <silent> <nowait> <buffer> D             :call <SID>RemoveBuffer("wipe")<CR>
     xnoremap <script> <silent> <nowait> <buffer> D             :call <SID>RemoveBuffer("wipe")<CR>
     nnoremap <script> <silent> <nowait> <buffer> f             :call <SID>SelectBuffer("split", "sb")<CR>
     nnoremap <script> <silent> <nowait> <buffer> F             :call <SID>SelectBuffer("split", "st")<CR>
-    nnoremap <script> <silent> <nowait> <buffer> m             :call <SID>MRUListShow()<CR>
     nnoremap <script> <silent> <nowait> <buffer> o             :call <SID>SelectBuffer()<CR>
     nnoremap <script> <silent> <nowait> <buffer> p             :call <SID>ToggleSplitOutPathName()<CR>
     nnoremap <script> <silent> <nowait> <buffer> q             :call <SID>Close()<CR>
@@ -643,7 +835,7 @@ function! s:CreateHelp()
         call add(header, '" <shift-enter> or t : open buffer in another tab')
         call add(header, '" a : toggle find active buffer')
         call add(header, '" b : Fast buffer switching with b<any bufnum>')
-        call add(header, '" B : toggle if to save/use recent tab or not')
+        call add(header, '" B : toggle showing buffers only on their MRU tabs')
         call add(header, '" d : delete buffer')
         call add(header, '" D : wipe buffer')
         call add(header, '" F : open buffer in another window above the current')
@@ -654,7 +846,7 @@ function! s:CreateHelp()
         call add(header, '" R : toggle showing relative or full paths')
         call add(header, '" s : cycle thru "sort by" fields '.string(s:sort_by).'')
         call add(header, '" S : reverse cycle thru "sort by" fields')
-        call add(header, '" T : toggle if to show only buffers for this tab or not')
+        call add(header, '" T : toggle showing all buffers/only buffers used on this tab')
         call add(header, '" u : toggle showing unlisted buffers')
         call add(header, '" V : open buffer in another window on the left of the current')
         call add(header, '" v : open buffer in another window on the right of the current')
@@ -756,6 +948,11 @@ function! s:BuildBufferList()
         " calculating other buffer details (e.g., `buf.fullname`) until we know
         " the user wants to view this buffer.
 
+        " Skip BufExplorer's buffer.
+        if buf._bufnr == s:bufExplorerBuffer
+            continue
+        endif
+
         " Skip unlisted buffers if we are not to show them.
         if !g:bufExplorerShowUnlisted && buf.attributes =~ "u"
             " Skip unlisted buffers if we are not to show them.
@@ -772,8 +969,8 @@ function! s:BuildBufferList()
             continue
         endif
 
-        " Are we to show only buffer(s) for this tab?
-        if g:bufExplorerShowTabBuffer && (!s:IsInCurrentTab(str2nr(buf.attributes)))
+        " Should we show this buffer in this tab?
+        if !s:MRUTabShouldShowBuf(s:tabIdAtLaunch, buf._bufnr)
             continue
         endif
 
@@ -807,6 +1004,11 @@ function! s:BuildBufferList()
 
     let lines = s:MakeLines(table)
     call setline(s:firstBufferLine, lines)
+    let firstMissingLine = s:firstBufferLine + len(lines)
+    if line('$') >= firstMissingLine
+        " Clear excess lines starting with `firstMissingLine`.
+        execute "silent keepjumps ".firstMissingLine.',$d _'
+    endif
     call s:SortListing()
 endfunction
 
@@ -907,9 +1109,6 @@ function! s:SelectBuffer(...)
             else
                 execute "999tab split +buffer" . _bufNbr
             endif
-
-            " Workaround for the issue mentioned in UpdateTabBufData.
-            call s:UpdateTabBufData(_bufNbr)
         " Are we supposed to open the selected buffer in a split?
         elseif (a:0 == 2) && (a:1 == "split")
             call s:Close()
@@ -1033,20 +1232,10 @@ function! s:DeleteBuffer(buf, mode)
     endtry
 endfunction
 
-" ListedAndCurrentTab {{{2
-" Returns whether the specified buffer is both listed and associated with the
-" current tab.
-function! s:ListedAndCurrentTab(buf)
-    return buflisted(a:buf) && s:IsInCurrentTab(a:buf)
-endfunction
-
 " Close {{{2
 function! s:Close()
-    " Get only the listed buffers associated with the current tab.
-    let listed = filter(copy(s:MRUList), "s:ListedAndCurrentTab(v:val)")
-    if len(listed) == 0
-        let listed = filter(range(1, bufnr('$')), "s:ListedAndCurrentTab(v:val)")
-    endif
+    " Get only the listed buffers associated with the current tab (up to 2).
+    let listed = s:MRUListedBuffersForTab(s:tabIdAtLaunch, 2)
 
     " If we needed to split the main window, close the split one.
     if s:didSplit
@@ -1093,8 +1282,11 @@ endfunction
 
 " ToggleShowTabBuffer {{{2
 function! s:ToggleShowTabBuffer()
+    " Forget any cached MRU ordering, as it depends on
+    " `g:bufExplorerShowTabBuffer`.
+    unlet! s:mruOrder
     let g:bufExplorerShowTabBuffer = !g:bufExplorerShowTabBuffer
-    call s:RebuildBufferList(g:bufExplorerShowTabBuffer)
+    call s:RebuildBufferList()
     call s:UpdateHelpStatus()
 endfunction
 
@@ -1108,7 +1300,7 @@ endfunction
 " ToggleShowUnlisted {{{2
 function! s:ToggleShowUnlisted()
     let g:bufExplorerShowUnlisted = !g:bufExplorerShowUnlisted
-    let num_bufs = s:RebuildBufferList(g:bufExplorerShowUnlisted == 0)
+    let num_bufs = s:RebuildBufferList()
     call s:UpdateHelpStatus()
 endfunction
 
@@ -1119,16 +1311,11 @@ function! s:ToggleFindActive()
 endfunction
 
 " RebuildBufferList {{{2
-function! s:RebuildBufferList(...)
+function! s:RebuildBufferList()
     setlocal modifiable
 
     let curPos = getpos('.')
 
-    if a:0 && a:000[0] && (line('$') >= s:firstBufferLine)
-        " Clear the list first.
-        execute "silent keepjumps ".s:firstBufferLine.',$d _'
-    endif
-
     let num_bufs = s:BuildBufferList()
 
     call setpos('.', curPos)
@@ -1184,10 +1371,7 @@ endfunction
 function! s:Key_mru(line)
     let _bufnr = str2nr(a:line)
     let buf = s:raw_buffer_listing[_bufnr]
-    let pos = index(s:MRUList, _bufnr)
-    if pos < 0
-        let pos = 0
-    endif
+    let pos = s:MRUOrderForBuf(_bufnr)
     return [printf('%9d', pos), buf.fullname]
 endfunction
 
@@ -1251,11 +1435,6 @@ function! s:SortListing()
     call s:SortByKeyFunc("<SID>Key_" . g:bufExplorerSortBy)
 endfunction
 
-" MRUListShow {{{2
-function! s:MRUListShow()
-    echomsg "MRUList=".string(s:MRUList)
-endfunction
-
 " Error {{{2
 " Display a message using ErrorMsg highlight group.
 function! s:Error(msg)
@@ -1321,6 +1500,21 @@ let g:BufExplorer_title = "\[Buf\ List\]"
 call s:Set("g:bufExplorerResize", 1)
 call s:Set("g:bufExplorerMaxHeight", 25) " Handles dynamic resizing of the window.
 
+" Evaluate a Vimscript expression in the context of this file.
+" This enables debugging of script-local variables and functions from outside
+" the plugin, e.g.:
+"   :echo BufExplorer_eval('s:bufMru')
+function! BufExplorer_eval(expr)
+    return eval(a:expr)
+endfunction
+
+" Execute a Vimscript statement in the context of this file.
+" This enables setting script-local variables from outside the plugin, e.g.:
+"   :call BufExplorer_execute('let s:bufMru = s:MRUNew(0)')
+function! BufExplorer_execute(statement)
+    execute a:statement
+endfunction
+
 " function! to start display. Set the mode to 'winmanager' for this buffer.
 " This is to figure out how this plugin was called. In a standalone fashion
 " or by winmanager.
@@ -1374,7 +1568,7 @@ call s:Set("g:bufExplorerDisableDefaultKeyMapping", 0)  " Do not disable default
 call s:Set("g:bufExplorerDefaultHelp", 1)               " Show default help?
 call s:Set("g:bufExplorerDetailedHelp", 0)              " Show detailed help?
 call s:Set("g:bufExplorerFindActive", 1)                " When selecting an active buffer, take you to the window where it is active?
-call s:Set("g:bufExplorerOnlyOneTab", 1)                " If ShowTabBuffer = 1, only store the most recent tab for this buffer.
+call s:Set("g:bufExplorerOnlyOneTab", 1)                " Show buffer only on MRU tab? (Applies when `g:bufExplorerShowTabBuffer` is true.)
 call s:Set("g:bufExplorerReverseSort", 0)               " Sort in reverse order by default?
 call s:Set("g:bufExplorerShowDirectories", 1)           " (Dir's are added by commands like ':e .')
 call s:Set("g:bufExplorerShowRelativePath", 0)          " Show listings with relative or absolute paths?