"============================================================================ " Copyright: Copyright (c) 2001-2025, Jeff Lanzarotta " All rights reserved. " " Redistribution and use in source and binary forms, with or " without modification, are permitted provided that the " following conditions are met: " " * Redistributions of source code must retain the above " copyright notice, this list of conditions and the following " disclaimer. " " * Redistributions in binary form must reproduce the above " copyright notice, this list of conditions and the following " disclaimer in the documentation and/or other materials " provided with the distribution. " " * Neither the name of the {organization} nor the names of its " contributors may be used to endorse or promote products " derived from this software without specific prior written " permission. " " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND " CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, " INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF " MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE " DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR " CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, " SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT " NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; " LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) " HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN " CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR " OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, " EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. " Name Of File: bufexplorer.vim " Description: Buffer Explorer Vim Plugin " Maintainer: Jeff Lanzarotta (my name at gmail dot com) " Last Changed: Wednesday, 05 March 2025 " Version: See g:bufexplorer_version for version number. " Usage: This file should reside in the plugin directory and be " automatically sourced. " " You may use the default keymappings of " " be - Opens BufExplorer " bt - Toggles BufExplorer open or closed " bs - Opens horizontally split window BufExplorer " bv - Opens vertically split window BufExplorer " " Or you can override the defaults and define your own mapping " in your vimrc file, for example: " " nnoremap :BufExplorer " nnoremap :ToggleBufExplorer " nnoremap :BufExplorerHorizontalSplit " nnoremap :BufExplorerVerticalSplit " " Or you can use " " ":BufExplorer" - Opens BufExplorer " ":ToggleBufExplorer" - Opens/Closes BufExplorer " ":BufExplorerHorizontalSplit" - Opens horizontally window BufExplorer " ":BufExplorerVerticalSplit" - Opens vertically split window BufExplorer " " For more help see supplied documentation. " History: See supplied documentation. "============================================================================= " Exit quickly if already running or when 'compatible' is set. {{{1 if exists("g:bufexplorer_version") || &cp finish endif "1}}} " Version number. let g:bufexplorer_version = "7.8.0" " Plugin Code {{{1 " Check for Vim version {{{2 if !exists("g:bufExplorerVersionWarn") let g:bufExplorerVersionWarn = 1 endif " Make sure we are using the correct version of Vim. If not, do not load the " plugin. if v:version < 704 if g:bufExplorerVersionWarn echohl WarningMsg echo "Sorry, bufexplorer ".g:bufexplorer_version." required Vim 7.4 or greater." echohl None endif finish endif " Create commands {{{2 command! BufExplorer :call BufExplorer() command! ToggleBufExplorer :call ToggleBufExplorer() command! BufExplorerHorizontalSplit :call BufExplorerHorizontalSplit() command! BufExplorerVerticalSplit :call BufExplorerVerticalSplit() " Set {{{2 function! s:Set(var, default) if !exists(a:var) if type(a:default) execute "let" a:var "=" string(a:default) else execute "let" a:var "=" a:default endif return 1 endif return 0 endfunction " Script variables {{{2 let s:MRU_Exclude_List = ["[BufExplorer]","__MRU_Files__","[Buf\ List]"] 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 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 " 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 " 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 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 " 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 " 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 " 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 endfunction " 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 " 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 " 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 " 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 " DoWinEnter {{{2 function! s:DoWinEnter() let bufNbr = str2nr(expand("")) 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 " DoBufEnter {{{2 function! s:DoBufEnter() let bufNbr = str2nr(expand("")) let tabNbr = tabpagenr() let tabId = s:MRUEnsureTabId(tabNbr) call s:MRUAddBufTab(bufNbr, tabId) endfunction " DoBufDelete {{{2 function! s:DoBufDelete() let bufNbr = str2nr(expand("")) call s:MRURemoveBuf(bufNbr) endfunction " DoTabClosed {{{2 function! s:DoTabClosed() call s:MRUGarbageCollectTabs() endfunction " ShouldIgnore {{{2 function! s:ShouldIgnore(buf) " Ignore temporary buffers with buftype set. if empty(getbufvar(a:buf, "&buftype")) == 0 return 1 endif " Ignore buffers with no name. if empty(bufname(a:buf)) == 1 return 1 endif " Ignore the BufExplorer buffer. if fnamemodify(bufname(a:buf), ":t") == s:name return 1 endif " Ignore any buffers in the exclude list. if index(s:MRU_Exclude_List, bufname(a:buf)) >= 0 return 1 endif " Else return 0 to indicate that the buffer was not ignored. return 0 endfunction " Initialize {{{2 function! s:Initialize() call s:SetLocalSettings() let s:running = 1 endfunction " Cleanup {{{2 function! s:Cleanup() if exists("s:_insertmode") let &insertmode = s:_insertmode endif if exists("s:_showcmd") let &showcmd = s:_showcmd endif if exists("s:_cpo") let &cpo = s:_cpo endif if exists("s:_report") let &report = s:_report endif let s:running = 0 let s:splitMode = "" let s:didSplit = 0 delmarks! endfunction " SetLocalSettings {{{2 function! s:SetLocalSettings() let s:_insertmode = &insertmode set noinsertmode let s:_showcmd = &showcmd set noshowcmd let s:_cpo = &cpo set cpo&vim let s:_report = &report let &report = 10000 setlocal nonumber setlocal foldcolumn=0 setlocal nofoldenable setlocal cursorline setlocal nospell setlocal nobuflisted setlocal filetype=bufexplorer endfunction " BufExplorerHorizontalSplit {{{2 function! BufExplorerHorizontalSplit() let s:splitMode = "sp" execute "BufExplorer" let s:splitMode = "" endfunction " BufExplorerVerticalSplit {{{2 function! BufExplorerVerticalSplit() let s:splitMode = "vsp" execute "BufExplorer" let s:splitMode = "" endfunction " ToggleBufExplorer {{{2 function! ToggleBufExplorer() if exists("s:running") && s:running == 1 && bufname(winbufnr(0)) == s:name call s:Close() else call BufExplorer() endif endfunction " BufExplorer {{{2 function! BufExplorer() let name = s:name if !has("win32") " On non-Windows boxes, escape the name so that is shows up correctly. let name = escape(name, "[]") endif " Make sure there is only one explorer open at a time. if s:running == 1 " Go to the open buffer. if has("gui") execute "drop" name endif return endif " 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. let [_splitbelow, _splitright] = [&splitbelow, &splitright] " Set the setting to ours. let [&splitbelow, &splitright] = [g:bufExplorerSplitBelow, g:bufExplorerSplitRight] let _size = (s:splitMode == "sp") ? g:bufExplorerSplitHorzSize : g:bufExplorerSplitVertSize " Split the window either horizontally or vertically. if _size <= 0 execute 'keepalt ' . s:splitMode else execute 'keepalt ' . _size . s:splitMode endif " Restore the original settings. let [&splitbelow, &splitright] = [_splitbelow, _splitright] " Remember that a split was triggered let s:didSplit = 1 endif if !exists("b:displayMode") || b:displayMode != "winmanager" " Do not use keepalt when opening bufexplorer to allow the buffer that " we are leaving to become the new alternate buffer 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 " the active buffer. The active buffer is the line with the '%' character " in it. execute search("%") endfunction " Tracks `tabId` at BufExplorer launch. let s:tabIdAtLaunch = '' " DisplayBufferList {{{2 function! s:DisplayBufferList() setlocal buftype=nofile setlocal modifiable setlocal noreadonly setlocal noswapfile setlocal nowrap setlocal bufhidden=wipe call s:SetupSyntax() call s:MapKeys() " Wipe out any existing lines in case BufExplorer buffer exists and the " user had changed any global settings that might reduce the number of " lines needed in the buffer. silent keepjumps 1,$d _ call setline(1, s:CreateHelp()) call s:BuildBufferList() call cursor(s:firstBufferLine, 1) if !g:bufExplorerResize normal! zz endif setlocal nomodifiable endfunction " MapKeys {{{2 function! s:MapKeys() if exists("b:displayMode") && b:displayMode == "winmanager" nnoremap :call SelectBuffer() endif nnoremap