"============================================================================ " 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: Friday, 11 April 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.12.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 " Command actions {{{2 let s:actions = [ \ 'current', \ 'close', \ 'split', \ 'vsplit', \ 'above', \ 'below', \ 'left', \ 'right', \ ] " Command-line completion function for `s:actions`. function! s:ActionArgs(ArgLead, CmdLine, CursorPos) return join(s:actions, "\n") endfunction " Create commands {{{2 command! -nargs=? -complete=custom,ActionArgs \ BufExplorer :call BufExplorer() command! -nargs=? -complete=custom,ActionArgs \ 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 " Naming conventions for file paths. " Conventionally a `path` is the string of characters used to identify a file " (ref. https://en.wikipedia.org/wiki/Path_(computing)). " An absolute or `full` path starts from the root directory and consists of " parent directories (if any) and a final `name` component. " A file's `dir` (directory) is the path to the parent directory of the file. " In general: " " fullpath = dir / name " " Paths below the user's home directory may be abbreviated, replacing the home " directory with `~`, e.g.: " " /home/user/some/file " -> " ~/some/file " " `homerel` refers to paths with home-directory-relative abbreviation. " " `relative` refers to paths computed relative to the current working directory; " these also include the home-directory-relative abbreviation. " " `rawpath` is the path as returned from `:buffers`; as such, buffers lacking " any name are represented as `[No Name]`. " " Thus, for a buffer: " - `rawpath` is the path as returned from `:buffers`. " - `fullpath` is the absolute path to the buffer. " - `homerelpath` is `fullpath` with the `~/` abbreviation. " - `relativepath` is `homerelpath` with relative abbreviation. " - `fulldir` is the absolute path to the buffer's parent directory. " - `homereldir` is `fulldir` with the `~/` abbreviation. " - `relativedir` is `homereldir` with relative abbreviation. " Script variables {{{2 let s:MRU_Exclude_List = ["[BufExplorer]","__MRU_Files__","[Buf\ List]"] let s:name = '[BufExplorer]' " 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:didSplit = 0 " 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: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() call BufExplorer('split') endfunction " BufExplorerVerticalSplit {{{2 function! BufExplorerVerticalSplit() call BufExplorer('vsplit') endfunction " ToggleBufExplorer {{{2 " Args: `([action])` " Optional `action` argument must be taken from `s:actions`. If not present, " `action` defaults to `g:bufExplorerDefaultAction`. function! ToggleBufExplorer(...) if a:0 >= 1 let action = a:1 else let action = g:bufExplorerDefaultAction endif if a:0 >= 2 echoerr 'Too many arguments' return endif if index(s:actions, action) < 0 echoerr 'Invalid action ' . action return endif if s:running && bufnr('%') == s:bufExplorerBuffer let action = 'close' endif call BufExplorer(action) endfunction " BufExplorer {{{2 " Args: `([action])` " Optional `action` argument must be taken from `s:actions`. If not present, " `action` defaults to `g:bufExplorerDefaultAction`. function! BufExplorer(...) if a:0 >= 1 let action = a:1 else let action = g:bufExplorerDefaultAction endif if a:0 >= 2 echoerr 'Too many arguments' return endif if index(s:actions, action) < 0 echoerr 'Invalid action ' . action return endif if action == 'close' call s:Close() return endif let [tabNbr, winNbr] = s:FindBufExplorer() if tabNbr > 0 execute 'keepjumps ' . tabNbr . 'tabnext' execute 'keepjumps ' . winNbr . 'wincmd w' return endif 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 let s:bufNbrAtLaunch = bufnr('%') let s:tabIdAtLaunch = s:MRUEnsureTabId(tabpagenr()) let s:windowAtLaunch = winnr() " Forget any cached MRU ordering from previous invocations. unlet! s:mruOrder let s:raw_buffer_listing = s:GetBufferInfo(0) call s:MRUGarbageCollectBufs() call s:MRUGarbageCollectTabs() " `{ action: [splitMode, botRight] }`. let actionMap = { \ 'split' : ['split', g:bufExplorerSplitBelow], \ 'vsplit' : ['vsplit', g:bufExplorerSplitRight], \ 'above' : ['split', 0], \ 'below' : ['split', 1], \ 'left' : ['vsplit', 0], \ 'right' : ['vsplit', 1], \ 'current' : ['', 0], \} let [splitMode, botRight] = actionMap[action] " We may have to split the current window. if splitMode != '' let size = splitMode == 'split' ? g:bufExplorerSplitHorzSize : g:bufExplorerSplitVertSize let cmd = 'keepalt ' . (botRight ? 'botright ' : 'topleft ') if size > 0 let cmd .= size endif let cmd .= splitMode execute cmd " 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 at BufExplorer launch (assuming it is displayed). let activeBufIndex = index(s:displayedBufNbrs, s:bufNbrAtLaunch) if activeBufIndex >= 0 let activeBufLineNbr = s:firstBufferLine + activeBufIndex keepjumps execute 'normal! ' . string(activeBufLineNbr) . 'G' endif if exists('#User#BufExplorer_Started') " Notify that BufExplorer has started. This is an opportunity to make " custom buffer-local mappings and the like. doautocmd User BufExplorer_Started endif endfunction " Tracks buffer number at BufExplorer launch. let s:bufNbrAtLaunch = 0 " Tracks `tabId` at BufExplorer launch. let s:tabIdAtLaunch = '' " Tracks window number at BufExplorer launch. let s:windowAtLaunch = 0 " DisplayBufferList {{{2 function! s:DisplayBufferList() setlocal buftype=nofile setlocal modifiable setlocal noreadonly setlocal noswapfile setlocal nowrap setlocal bufhidden=wipe 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 " BufExplorer_redisplay {{{2 function! BufExplorer_redisplay() if s:running && bufnr('%') == s:bufExplorerBuffer call s:RedisplayBufferList() endif endfunction " RedisplayBufferList {{{2 function! s:RedisplayBufferList() call s:RebuildBufferList() call s:UpdateHelpStatus() endfunction " MapKeys {{{2 function! s:MapKeys() nnoremap (BufExplorer_BufferDelete) :call RemoveBuffer("delete") nnoremap (BufExplorer_BufferDeleteForced) :call RemoveBuffer("force_delete") nnoremap (BufExplorer_BufferWipe) :call RemoveBuffer("wipe") nnoremap (BufExplorer_BufferWipeForced) :call RemoveBuffer("force_wipe") nnoremap (BufExplorer_Close) :call Close() nnoremap (BufExplorer_OpenBuffer) :call SelectBuffer() nnoremap (BufExplorer_OpenBufferAsk) :call SelectBuffer("ask") nnoremap (BufExplorer_OpenBufferOriginalWindow) :call SelectBuffer("original_window") nnoremap (BufExplorer_OpenBufferSplitAbove) :call SelectBuffer("split", "st") nnoremap (BufExplorer_OpenBufferSplitBelow) :call SelectBuffer("split", "sb") nnoremap (BufExplorer_OpenBufferSplitLeft) :call SelectBuffer("split", "vl") nnoremap (BufExplorer_OpenBufferSplitRight) :call SelectBuffer("split", "vr") nnoremap (BufExplorer_OpenBufferTab) :call SelectBuffer("tab") nnoremap (BufExplorer_SortByNext) :call SortSelect() nnoremap (BufExplorer_SortByPrev) :call ReverseSortSelect() nnoremap (BufExplorer_ToggleFindActive) :call ToggleFindActive() nnoremap (BufExplorer_ToggleHelp) :call ToggleHelp() nnoremap (BufExplorer_ToggleOnlyOneTab) :call ToggleOnlyOneTab() nnoremap (BufExplorer_ToggleReverseSort) :call SortReverse() nnoremap (BufExplorer_ToggleShowRelativePath) :call ToggleShowRelativePath() nnoremap (BufExplorer_ToggleShowTabBuffer) :call ToggleShowTabBuffer() nnoremap (BufExplorer_ToggleShowTerminal) :call ToggleShowTerminal() nnoremap (BufExplorer_ToggleShowUnlisted) :call ToggleShowUnlisted() nnoremap (BufExplorer_ToggleSplitOutPathName) :call ToggleSplitOutPathName() if exists("b:displayMode") && b:displayMode == "winmanager" nnoremap :call SelectBuffer() endif nmap <2-leftmouse> (BufExplorer_OpenBuffer) nmap (BufExplorer_OpenBuffer) nmap (BufExplorer_ToggleHelp) nmap (BufExplorer_OpenBufferTab) nmap a (BufExplorer_ToggleFindActive) nmap b (BufExplorer_OpenBufferAsk) nmap B (BufExplorer_ToggleOnlyOneTab) nmap d (BufExplorer_BufferDelete) nmap D (BufExplorer_BufferWipe) nmap f (BufExplorer_OpenBufferSplitBelow) nmap F (BufExplorer_OpenBufferSplitAbove) nmap o (BufExplorer_OpenBuffer) nmap O (BufExplorer_OpenBufferOriginalWindow) nmap p (BufExplorer_ToggleSplitOutPathName) nmap q (BufExplorer_Close) nmap r (BufExplorer_ToggleReverseSort) nmap R (BufExplorer_ToggleShowRelativePath) nmap s (BufExplorer_SortByNext) nmap S (BufExplorer_SortByPrev) nmap t (BufExplorer_OpenBufferTab) nmap T (BufExplorer_ToggleShowTabBuffer) nmap u (BufExplorer_ToggleShowUnlisted) nmap v (BufExplorer_OpenBufferSplitRight) nmap V (BufExplorer_OpenBufferSplitLeft) nmap X (BufExplorer_ToggleShowTerminal) for k in ["G", "n", "N", "L", "M", "H"] execute "nnoremap " k ":keepjumps normal!" k."" endfor endfunction " ToggleHelp {{{2 function! s:ToggleHelp() let g:bufExplorerDetailedHelp = !g:bufExplorerDetailedHelp setlocal modifiable " Save position. normal! ma " Remove old header. if s:firstBufferLine > 1 execute "keepjumps 1,".(s:firstBufferLine - 1) "d _" endif call append(0, s:CreateHelp()) silent! normal! g`a delmarks a setlocal nomodifiable if exists("b:displayMode") && b:displayMode == "winmanager" call WinManagerForceReSize("BufExplorer") endif endfunction " GetHelpStatus {{{2 function! s:GetHelpStatus() let ret = '" Sorted by '.((g:bufExplorerReverseSort == 1) ? "reverse " : "").g:bufExplorerSortBy let ret .= ' | '.((g:bufExplorerFindActive == 0) ? "Don't " : "")."Locate buffer" let ret .= ((g:bufExplorerShowUnlisted == 0) ? "" : " | Show unlisted") let ret .= ((g:bufExplorerShowTabBuffer == 0) ? "" : " | Show buffers/tab") let ret .= ((g:bufExplorerOnlyOneTab == 0) ? "" : " | One tab/buffer") let ret .= ' | ' if g:bufExplorerShowRelativePath let ret .= "Relative " endif let ret .= ((g:bufExplorerSplitOutPathName == 0) ? "Whole" : "Split")." path" let ret .= ((g:bufExplorerShowTerminal == 0) ? "" : " | Show terminal") return ret endfunction " CreateHelp {{{2 function! s:CreateHelp() if g:bufExplorerDefaultHelp == 0 && g:bufExplorerDetailedHelp == 0 let s:firstBufferLine = 1 return [] endif let header = [] if g:bufExplorerDetailedHelp == 1 call add(header, '" Buffer Explorer ('.g:bufexplorer_version.')') call add(header, '" --------------------------') call add(header, '" : toggle this help') call add(header, '" or o or Mouse-Double-Click : open buffer under cursor') call add(header, '" or t : open buffer in another tab') call add(header, '" a : toggle find active buffer') call add(header, '" b : Fast buffer switching with b') 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') call add(header, '" f : open buffer in another window below the current') call add(header, '" O : open buffer in original window') call add(header, '" p : toggle splitting of path into name + dir') call add(header, '" q : quit') call add(header, '" r : reverse sort') call add(header, '" R : toggle showing relative 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 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') call add(header, '" X : toggle showing terminal buffers') else call add(header, '" Press for Help') endif if (!exists("b:displayMode") || b:displayMode != "winmanager") || (b:displayMode == "winmanager" && g:bufExplorerDetailedHelp == 1) call add(header, s:GetHelpStatus()) call add(header, '"=') endif let s:firstBufferLine = len(header) + 1 return header endfunction " CalculateBufferDetails {{{2 " Calculate `buf`-related details. " Only these fields of `buf` must be defined on entry: " - `.bufNbr` " - `.numberindicators` " - `.line` function! s:CalculateBufferDetails(buf) let buf = a:buf let buf.number = string(buf.bufNbr) let buf.indicators = substitute(buf.numberindicators, '^\s*\d*', '', '') let rawpath = bufname(buf.bufNbr) let buf["hasNoName"] = empty(rawpath) if buf.hasNoName let rawpath = "[No Name]" let buf.isdir = 0 else let buf.isdir = getftype(rawpath) == 'dir' endif let buf.rawpath = rawpath let buf.isterminal = getbufvar(buf.bufNbr, '&buftype') == 'terminal' if buf.isterminal " Neovim uses paths with `term://` prefix, where the provided dir path " is the current working directory when the terminal was launched, e.g.: " - Unix: " term://~/tmp/sort//1464953:/bin/bash " - Windows: " term://C:\apps\nvim-win64\bin//6408:C:\Windows\system32\cmd.exe " Vim uses paths starting with `!`, with no provided dir path, e.g.: " - Unix: " !/bin/bash " - Windows: " !C:\Windows\system32\cmd.exe " Use the terminal's current working directory as `fulldir`. " For `name`, use `!PID:shellName`, prefixed with `!` as Vim does, " and without the shell's dir path for brevity, e.g.: " `/bin/bash` -> `!bash` " `1464953:/bin/bash` -> `!1464953:bash` " `C:\Windows\system32\cmd.exe` -> `!cmd.exe` " `6408:C:\Windows\system32\cmd.exe` -> `!6408:cmd.exe` " Neovim-style path format: " term://(cwd)//(pid):(shellPath) " e.g.: " term://~/tmp/sort//1464953:/bin/bash " `cwd` is the directory at terminal launch. let termNameParts = matchlist(rawpath, '\v\c^term://(.*)//(\d+):(.*)$') if len(termNameParts) > 0 let [cwd, pidStr, shellPath] = termNameParts[1:3] let pid = str2nr(pidStr) let shellName = fnamemodify(shellPath, ':t') else " Default to Vim's current working directory. let cwd = '.' let shellName = fnamemodify(rawpath, ':t') let pid = -1 if exists('*term_getjob') && exists('*job_info') let job = term_getjob(buf.bufNbr) if job != v:null let pid = job_info(job).process endif endif endif if pid < 0 let name = '!' . shellName else let name = '!' . pid . ':' . shellName " On some systems having a `/proc` filesystem (e.g., Linux, *BSD, " Solaris), each process has a `cwd` symlink for the current working " directory. `resolve()` will return the actual current working " directory if possible; otherwise, it will return the symlink path " unchanged. let cwd_symlink = '/proc/' . pid . '/cwd' let resolved_cwd = resolve(cwd_symlink) if resolved_cwd != cwd_symlink let cwd = resolved_cwd endif endif let slashed_cwd = fnamemodify(cwd, ':p') let buf.fullpath = slashed_cwd . name let buf.fulldir = fnamemodify(slashed_cwd, ':h') let buf.name = name let buf.homereldir = fnamemodify(slashed_cwd, ':~:h') let buf.homerelpath = fnamemodify(buf.fullpath, ':~') let buf.relativedir = fnamemodify(slashed_cwd, ':~:.:h') let buf.relativepath = fnamemodify(buf.fullpath, ':~:.') return endif let fullpath = simplify(fnamemodify(rawpath, ':p')) if buf.isdir " `fullpath` ends with a path separator; this will be " removed via the first `:h` applied to `fullpath` (except " for the root directory, where the path separator will remain). " Must perform shortening (`:~`, `:.`) before `:h`. let buf.homerelpath = fnamemodify(fullpath, ':~:h') let buf.relativepath = fnamemodify(fullpath, ':~:.:h') " Remove trailing slash. let fullpath = fnamemodify(fullpath, ':h') let parent = fnamemodify(fullpath, ':h') let buf.name = fnamemodify(fullpath, ':t') " Special case for root directory: fnamemodify('/', ':h:t') == '' if buf.name == '' let buf.name = '.' endif else let parent = fnamemodify(fullpath, ':h') let buf.name = fnamemodify(fullpath, ':t') let buf.homerelpath = fnamemodify(fullpath, ':~') let buf.relativepath = fnamemodify(fullpath, ':~:.') endif let buf.fullpath = fullpath let buf.fulldir = parent " `:p` on `parent` adds back the path separator which permits more " effective shortening (`:~`, `:.`), but `:h` is required afterward " to trim this separator. let buf.homereldir = fnamemodify(parent, ':p:~:h') let buf.relativedir = fnamemodify(parent, ':p:~:.:h') endfunction " GetBufferInfo {{{2 " Return dictionary `{ bufNbr : buf }`. " - If `onlyBufNbr > 0`, dictionary will contain at most that buffer. " On return, only these fields are set for each `buf`: " - `.bufNbr` " - `.numberindicators` " - `.line` " Other fields will be populated by `s:CalculateBufferDetails()`. function! s:GetBufferInfo(onlyBufNbr) redir => bufoutput " Below, `:silent buffers` allows capturing the output via `:redir` but " prevents display to the user. if a:onlyBufNbr > 0 && buflisted(a:onlyBufNbr) " We care only about the listed buffer `a:onlyBufNbr`, so no need to " enumerate unlisted buffers. silent buffers else " Use `!` to show all buffers including the unlisted ones. silent buffers! endif redir END if a:onlyBufNbr > 0 " Since we are only interested in this specified buffer remove the " other buffers listed. " Use a very-magic pattern starting with a newline and a run of zero or " more spaces/tabs: let onlyLinePattern = '\v\n\s*' " Continue with the buffer number followed by a non-digit character " (which will be a buffer indicator character such as `u` or ` `). let onlyLinePattern .= a:onlyBufNbr . '\D' " Finish with a run of zero or more non-newline characters plus newline: let onlyLinePattern .= '[^\n]*\n' let bufoutput = matchstr("\n" . bufoutput . "\n", onlyLinePattern) endif let all = {} " Loop over each line in the buffer. for line in split(bufoutput, '\n') let bits = split(line, '"') " Use first and last components after the split on '"', in case a " filename with an embedded '"' is present. let buf = { \ "numberindicators": bits[0], \ "line": substitute(bits[-1], \ '\s*', '', '') \} let buf.bufNbr = str2nr(buf.numberindicators) let all[buf.bufNbr] = buf endfor return all endfunction " BuildBufferList {{{2 function! s:BuildBufferList() if exists('#User#BufExplorer_PreDisplay') " Notify that BufExplorer is about to display the buffer list. This is " an opportunity to make last-minute changes to `g:bufExplorerColumns`. doautocmd User BufExplorer_PreDisplay endif let columns = s:GetColumns() let table = [] let s:displayedBufNbrs = [] " Loop through every buffer. for buf in values(s:raw_buffer_listing) " `buf.numberindicators` must exist, but we defer the expensive work of " calculating other buffer details (e.g., `buf.fullpath`) until we know " the user wants to view this buffer. " Skip BufExplorer's buffer. if buf.bufNbr == s:bufExplorerBuffer continue endif " Skip unlisted buffers if we are not to show them. if !g:bufExplorerShowUnlisted && buf.numberindicators =~ "u" " Skip unlisted buffers if we are not to show them. continue endif " Ensure buffer details are computed for this buffer. if !has_key(buf, 'fullpath') call s:CalculateBufferDetails(buf) endif " Skip 'No Name' buffers if we are not to show them. if g:bufExplorerShowNoName == 0 && buf.hasNoName continue endif " Should we show this buffer in this tab? if !s:MRUTabShouldShowBuf(s:tabIdAtLaunch, buf.bufNbr) continue endif " Skip terminal buffers if we are not to show them. if !g:bufExplorerShowTerminal && buf.isterminal continue endif " Skip directory buffers if we are not to show them. if !g:bufExplorerShowDirectories && buf.isdir continue endif let row = [] for column in columns if has_key(buf, column) let row += [buf[column]] elseif column == 'icon' " Support must exist or 'icon' would have been removed. let row += [WebDevIconsGetFileTypeSymbol(buf.fullpath, buf.isdir)] else " Must be of the form `=literal`. let row += [column[1:]] endif endfor call add(table, row) call add(s:displayedBufNbrs, buf.bufNbr) endfor 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 " Buffer numbers for buffers displayed in the BufExplorer window. let s:displayedBufNbrs = [] " GetColumns {{{2 function! s:GetColumns() let validColumns = [ \ 'dir', \ 'fulldir', \ 'fullpath', \ 'homereldir', \ 'homerelpath', \ 'icon', \ 'indicators', \ 'line', \ 'name', \ 'number', \ 'numberindicators', \ 'path', \ 'rawpath', \ 'relativedir', \ 'relativepath', \ 'splittablepath', \ ] let columns = [] for column in g:bufExplorerColumns if column == 'splittablepath' if g:bufExplorerSplitOutPathName let columns += ['name'] let column = 'dir' else let column = 'path' endif endif if column == 'dir' || column == 'path' if g:bufExplorerShowRelativePath let column = 'relative' . column else let column = 'homerel' . column endif endif if column == 'icon' " 'icon' is always valid, but omitted unless support is loaded. if exists("g:loaded_webdevicons") let columns += [column] endif elseif index(validColumns, column) >= 0 || column =~# '^=' let columns += [column] else let columns += ['=[bad column name "' . column . '"]'] endif endfor if len(columns) == 0 let columns = ['=[`g:bufExplorerColumns` is empty]'] endif return columns endfunction " BufExplorer_defaultColumns {{{2 " User-accessible default value for `g:bufExplorerColumns`. " Implemented as a function so that users may modify the returned list. function! BufExplorer_defaultColumns() let defaultColumns = [ \ 'numberindicators', \ 'icon', \ 'splittablepath', \ 'line', \ ] return defaultColumns endfunction " MakeLines {{{2 function! s:MakeLines(table) if len(a:table) == 0 return [] endif let lines = [] " To avoid trailing whitespace, do not pad the final column. let numColumnsToPad = len(a:table[0]) - 1 " Ensure correctness even if `table` has no columns. if numColumnsToPad < 0 let numColumnsToPad = 0 endif let maxWidths = repeat([0], numColumnsToPad) for row in a:table let i = 0 while i < numColumnsToPad let maxWidths[i] = max([maxWidths[i], s:StringWidth(row[i])]) let i = i + 1 endwhile endfor let pads = [] for w in maxWidths call add(pads, repeat(' ', w)) endfor for row in a:table let i = 0 while i < numColumnsToPad let row[i] .= strpart(pads[i], s:StringWidth(row[i])) let i = i + 1 endwhile call add(lines, join(row, ' ')) endfor return lines endfunction " SelectBuffer {{{2 " Valid arguments: " `()` Open in current window. " `("ask")` Prompt for buffer, then open in current window. " `("original_window")` Open in original window. " `("split", "st")` Open in horizontal split above current window. " `("split", "sb")` Open in horizontal split below current window. " `("split", "vl")` Open in vertical split left of current window. " `("split", "vr")` Open in vertical split right of current window. " `("tab")` Open in a new tab. function! s:SelectBuffer(...) " Sometimes messages are not cleared when we get here so it looks like an " error has occurred when it really has not. "echo "" let bufNbr = -1 if (a:0 == 1) && (a:1 == "ask") " Ask the user for input. call inputsave() let cmd = input("Enter buffer number to switch to: ") call inputrestore() " Clear the message area from the previous prompt. redraw | echo if strlen(cmd) > 0 let bufNbr = str2nr(cmd) else call s:Error("Invalid buffer number, try again.") return endif else let bufNbr = s:GetBufNbrAtCursor() if bufNbr == 0 return endif " Check and see if we are running BufferExplorer via WinManager. if exists("b:displayMode") && b:displayMode == "winmanager" let _bufName = expand("#".bufNbr.":p") if (a:0 == 1) && (a:1 == "tab") call WinManagerFileEdit(_bufName, 1) else call WinManagerFileEdit(_bufName, 0) endif return endif endif if bufexists(bufNbr) " Get the tab number where this buffer is located in. let tabNbr = s:GetTabNbr(bufNbr) if exists("g:bufExplorerChgWin") && g:bufExplorerChgWin <=winnr("$") execute g:bufExplorerChgWin."wincmd w" execute "keepjumps keepalt silent b!" bufNbr " Are we supposed to open the selected buffer in a tab? elseif (a:0 == 1) && (a:1 == "tab") call s:Close() " Open a new tab with the selected buffer in it. if v:version > 704 || ( v:version == 704 && has('patch2237') ) " new syntax for last tab as of 7.4.2237 execute "$tab split +buffer" . bufNbr else execute "999tab split +buffer" . bufNbr endif " Are we supposed to open the selected buffer in a split? elseif (a:0 == 2) && (a:1 == "split") call s:Close() if (a:2 == "vl") execute "vert topleft sb ".bufNbr elseif (a:2 == "vr") execute "vert belowright sb ".bufNbr elseif (a:2 == "st") execute "topleft sb ".bufNbr else " = sb execute "belowright sb ".bufNbr endif " Are we supposed to open the selected buffer in the original window? elseif (a:0 == 1) && (a:1 == "original_window") call s:Close() execute s:windowAtLaunch . "wincmd w" execute "keepjumps keepalt silent b!" bufNbr else " Request to open in current (BufExplorer) window. if g:bufExplorerFindActive && tabNbr > 0 " Close BufExplorer window and switch to existing tab/window. call s:Close() execute tabNbr . "tabnext" execute bufwinnr(bufNbr) . "wincmd w" else " Use BufExplorer window for the buffer. execute "keepjumps keepalt silent b!" bufNbr endif endif " Make the buffer 'listed' again. call setbufvar(bufNbr, "&buflisted", "1") " Call any associated function references. g:bufExplorerFuncRef may be " an individual function reference or it may be a list containing " function references. It will ignore anything that's not a function " reference. " " See :help FuncRef for more on function references. if exists("g:BufExplorerFuncRef") if type(g:BufExplorerFuncRef) == 2 keepj call g:BufExplorerFuncRef() elseif type(g:BufExplorerFuncRef) == 3 for FncRef in g:BufExplorerFuncRef if type(FncRef) == 2 keepj call FncRef() endif endfor endif endif else call s:Error("Sorry, that buffer no longer exists, please select another") call s:DeleteBuffer(bufNbr, "wipe") endif endfunction " RemoveBuffer {{{2 " Valid `mode` values: " - "delete" " - "force_delete" " - "wipe" " - "force_wipe" function! s:RemoveBuffer(mode) " Are we on a line with a file name? if line('.') < s:firstBufferLine return endif let mode = a:mode let forced = mode =~# '^force_' " These commands are to temporarily suspend the activity of winmanager. if exists("b:displayMode") && b:displayMode == "winmanager" call WinManagerSuspendAUs() end let bufNbr = s:GetBufNbrAtCursor() if bufNbr == 0 return endif let buf = s:raw_buffer_listing[bufNbr] if !forced && (buf.isterminal || getbufvar(bufNbr, '&modified')) if buf.isterminal let msg = "Buffer " . bufNbr . " is a terminal" else let msg = "No write since last change for buffer " . bufNbr endif " Calling confirm() requires Vim built with dialog option. if !has("dialog_con") && !has("dialog_gui") call s:Error(msg . "; cannot remove without 'force'") return endif let answer = confirm(msg . "; Remove anyway?", "&Yes\n&No", 2) if answer == 1 let mode = 'force_' . mode else return endif endif " Okay, everything is good, delete or wipe the buffer. call s:DeleteBuffer(bufNbr, mode) " Reactivate winmanager autocommand activity. if exists("b:displayMode") && b:displayMode == "winmanager" call WinManagerForceReSize("BufExplorer") call WinManagerResumeAUs() end endfunction " DeleteBuffer {{{2 " Valid `mode` values: " - "delete" " - "force_delete" " - "wipe" " - "force_wipe" function! s:DeleteBuffer(bufNbr, mode) " This routine assumes that the buffer to be removed is on the current line. if a:mode =~# 'delete$' && bufexists(a:bufNbr) && !buflisted(a:bufNbr) call s:Error('Buffer ' . a:bufNbr \ . ' is unlisted; must `wipe` to remove') return endif try " Wipe/Delete buffer from Vim. if a:mode == "wipe" execute "silent bwipe" a:bufNbr elseif a:mode == "force_wipe" execute "silent bwipe!" a:bufNbr elseif a:mode == "force_delete" execute "silent bdelete!" a:bufNbr else execute "silent bdelete" a:bufNbr endif catch call s:Error(v:exception) endtry if bufexists(a:bufNbr) " Buffer is still present. We may have failed to wipe it, or it may " have changed indicators (as `:bd` only makes a buffer unlisted). " Regather information on this buffer, update the buffer list, and " redisplay. let info = s:GetBufferInfo(a:bufNbr) let s:raw_buffer_listing[a:bufNbr] = info[a:bufNbr] call s:RedisplayBufferList() else " Delete the buffer from the list on screen. setlocal modifiable normal! "_dd setlocal nomodifiable " Delete the buffer from the raw buffer list. unlet s:raw_buffer_listing[a:bufNbr] " Remove buffer number from list of displayed buffer numbers. call remove(s:displayedBufNbrs, index(s:displayedBufNbrs, a:bufNbr)) endif endfunction " Close {{{2 function! s:Close() let [tabNbr, winNbr] = s:FindBufExplorer() if tabNbr == 0 return endif let [curTabNbr, curWinNbr] = [tabpagenr(), winnr()] if [tabNbr, winNbr] != [curTabNbr, curWinNbr] " User has switched away from the original BufExplorer window. " It's unclear how to do better than simply wiping out the " BufExplorer buffer. execute 'bwipeout ' . s:bufExplorerBuffer return 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 execute "wincmd c" endif " Check to see if there are anymore buffers listed. if len(listed) == 0 " Since there are no buffers left to switch to, open a new empty " buffers. execute "enew" else " Since there are buffers left to switch to, switch to the previous and " then the current. for b in reverse(listed[0:1]) execute "keepjumps silent b ".b endfor endif " Clear any messages. echo endfunction " FindBufExplorer {{{2 " Return `[tabNbr, winNbr]`; both numbers will be zero if not found. function! s:FindBufExplorer() let result = [0, 0] if s:running let numTabs = tabpagenr('$') for tabNbr in range(1, numTabs) let winNbr = index(tabpagebuflist(tabNbr), s:bufExplorerBuffer) + 1 if winNbr > 0 let result = [tabNbr, winNbr] break endif endfor endif return result endfunction " ToggleShowTerminal {{{2 function! s:ToggleShowTerminal() let g:bufExplorerShowTerminal = !g:bufExplorerShowTerminal call s:RedisplayBufferList() endfunction " ToggleSplitOutPathName {{{2 function! s:ToggleSplitOutPathName() let g:bufExplorerSplitOutPathName = !g:bufExplorerSplitOutPathName call s:RedisplayBufferList() endfunction " ToggleShowRelativePath {{{2 function! s:ToggleShowRelativePath() let g:bufExplorerShowRelativePath = !g:bufExplorerShowRelativePath call s:RedisplayBufferList() 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:RedisplayBufferList() endfunction " ToggleOnlyOneTab {{{2 function! s:ToggleOnlyOneTab() let g:bufExplorerOnlyOneTab = !g:bufExplorerOnlyOneTab call s:RedisplayBufferList() endfunction " ToggleShowUnlisted {{{2 function! s:ToggleShowUnlisted() let g:bufExplorerShowUnlisted = !g:bufExplorerShowUnlisted call s:RedisplayBufferList() endfunction " ToggleFindActive {{{2 function! s:ToggleFindActive() let g:bufExplorerFindActive = !g:bufExplorerFindActive call s:UpdateHelpStatus() endfunction " RebuildBufferList {{{2 function! s:RebuildBufferList() setlocal modifiable let curPos = getpos('.') let num_bufs = s:BuildBufferList() call setpos('.', curPos) setlocal nomodifiable return num_bufs endfunction " UpdateHelpStatus {{{2 function! s:UpdateHelpStatus() setlocal modifiable let text = s:GetHelpStatus() call setline(s:firstBufferLine - 2, text) setlocal nomodifiable endfunction " Key_number {{{2 function! s:Key_number(buf) let key = [printf('%020d', a:buf.bufNbr)] return key endfunction " Key_name {{{2 function! s:Key_name(buf) let key = [a:buf.name, a:buf.fullpath] return key endfunction " Key_fullpath {{{2 function! s:Key_fullpath(buf) let key = [a:buf.fullpath] return key endfunction " Key_extension {{{2 function! s:Key_extension(buf) let extension = fnamemodify(a:buf.name, ':e') let key = [extension, a:buf.name, a:buf.fullpath] return key endfunction " Key_mru {{{2 function! s:Key_mru(buf) let pos = s:MRUOrderForBuf(a:buf.bufNbr) return [printf('%9d', pos), a:buf.fullpath] endfunction " SortByKeyFunc {{{2 function! s:SortByKeyFunc(keyFunc) let lastLineNbr = s:firstBufferLine + len(s:displayedBufNbrs) - 1 " `s:firstBufferLine >= 1`, `lastLineNbr >= 0`; `getline(1, 0) -> []`. let displayedLines = getline(s:firstBufferLine, lastLineNbr) let bufLineIndex = 0 let keyedLines = [] for bufNbr in s:displayedBufNbrs let buf = s:raw_buffer_listing[bufNbr] let key = eval(a:keyFunc . '(buf)') let parts = key + [string(bufNbr), string(bufLineIndex)] call add(keyedLines, join(parts, "\1")) let bufLineIndex += 1 endfor " Ignore case when sorting by passing `1`: call sort(keyedLines, 1) if g:bufExplorerReverseSort call reverse(keyedLines) endif let s:displayedBufNbrs = [] let lines = [] for keyedLine in keyedLines let parts = split(keyedLine, "\1") let [bufNbrStr, bufLineIndexStr] = parts[-2:-1] call add(s:displayedBufNbrs, str2nr(bufNbrStr)) call add(lines, displayedLines[str2nr(bufLineIndexStr)]) endfor call setline(s:firstBufferLine, lines) endfunction " SortReverse {{{2 function! s:SortReverse() let g:bufExplorerReverseSort = !g:bufExplorerReverseSort call s:ReSortListing() endfunction " SortSelect {{{2 function! s:SortSelect() let g:bufExplorerSortBy = get(s:sort_by, index(s:sort_by, g:bufExplorerSortBy) + 1, s:sort_by[0]) call s:ReSortListing() endfunction " ReverseSortSelect {{{2 function! s:ReverseSortSelect() let g:bufExplorerSortBy = get(s:sort_by, index(s:sort_by, g:bufExplorerSortBy) - 1, s:sort_by[-1]) call s:ReSortListing() endfunction " ReSortListing {{{2 function! s:ReSortListing() setlocal modifiable let curPos = getpos('.') call s:SortListing() call s:UpdateHelpStatus() call setpos('.', curPos) setlocal nomodifiable endfunction " SortListing {{{2 function! s:SortListing() call s:SortByKeyFunc("Key_" . g:bufExplorerSortBy) endfunction " GetBufNbrAtCursor {{{2 " Return `bufNbr` at cursor; return 0 if no buffer on that line. function! s:GetBufNbrAtCursor() return s:GetBufNbrAtLine(line('.')) endfunction " GetBufNbrAtLine {{{2 " Return `bufNbr` at `lineNbr`; return 0 if no buffer on that line. function! s:GetBufNbrAtLine(lineNbr) let bufIndex = a:lineNbr - s:firstBufferLine if bufIndex < 0 || bufIndex >= len(s:displayedBufNbrs) return 0 endif return s:displayedBufNbrs[bufIndex] endfunction " BufferNumLines {{{2 " Return number of lines in the BufExplorer buffer. function! s:BufferNumLines() " `line('$')` returns the line number of the last line in a buffer. " Normally, this is the same as the number of lines in the buffer. When " there are no lines in the buffer, logically `line('$')` should return " zero, but Vim unfortunately returns 1 for this case. This is because " there must always be a valid line number for the cursor. " " When `line('$') == 1`, we detect an empty buffer by seeing if the first " line itself is empty. Technically, this cannot distinguish a completely " empty buffer from a one-line buffer with no characters on the first line, " but BufExplorer doesn't create empty lines in the buffer. let numLines = line('$') if numLines == 1 && getline(1) == '' let numLines = 0 endif return numLines endfunction " Error {{{2 " Display a message using ErrorMsg highlight group. function! s:Error(msg) echohl ErrorMsg echomsg a:msg echohl None endfunction " Warning {{{2 " Display a message using WarningMsg highlight group. function! s:Warning(msg) echohl WarningMsg echomsg a:msg echohl None endfunction " GetTabNbr {{{2 function! s:GetTabNbr(bufNbr) " Prefer current tab. if bufwinnr(a:bufNbr) > 0 return tabpagenr() endif " Searching buffer bufno, in tabs. for i in range(tabpagenr("$")) if index(tabpagebuflist(i + 1), a:bufNbr) != -1 return i + 1 endif endfor return 0 endfunction " GetWinNbr" {{{2 function! s:GetWinNbr(tabNbr, bufNbr) " window number in tabpage. let tablist = tabpagebuflist(a:tabNbr) " Number: 0 " String: 1 " Funcref: 2 " List: 3 " Dictionary: 4 " Float: 5 if type(tablist) == 3 return index(tabpagebuflist(a:tabNbr), a:bufNbr) + 1 else return 1 endif endfunction " StringWidth" {{{2 if exists('*strwidth') function s:StringWidth(s) return strwidth(a:s) endfunction else function s:StringWidth(s) return len(a:s) endfunction endif " Winmanager Integration {{{2 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. function! BufExplorer_Start() let b:displayMode = "winmanager" call s:SetLocalSettings() call BufExplorer() endfunction " Returns whether the display is okay or not. function! BufExplorer_IsValid() return 0 endfunction " Handles dynamic refreshing of the window. function! BufExplorer_Refresh() let b:displayMode = "winmanager" call s:SetLocalSettings() call BufExplorer() endfunction function! BufExplorer_ReSize() if !g:bufExplorerResize return end let nlines = min([line("$"), g:bufExplorerMaxHeight]) execute nlines." wincmd _" " The following lines restore the layout so that the last file line is also " the last window line. Sometimes, when a line is deleted, although the " window size is exactly equal to the number of lines in the file, some of " the lines are pushed up and we see some lagging '~'s. let pres = getpos(".") normal! $ let _scr = &scrolloff let &scrolloff = 0 normal! z- let &scrolloff = _scr call setpos(".", pres) endfunction " Default values {{{2 call s:Set("g:bufExplorerColumns", BufExplorer_defaultColumns()) " Configurable list of column names for the buffer list. call s:Set("g:bufExplorerDisableDefaultKeyMapping", 0) " Do not disable default key mappings. call s:Set("g:bufExplorerDefaultAction", 'current') " Default action for `:BufExplorer` with no args. 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) " 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? call s:Set("g:bufExplorerShowTabBuffer", 0) " Show only buffer(s) for this tab? call s:Set("g:bufExplorerShowUnlisted", 0) " Show unlisted buffers? call s:Set("g:bufExplorerShowNoName", 0) " Show 'No Name' buffers? call s:Set("g:bufExplorerSortBy", "mru") " Sorting methods are in s:sort_by: call s:Set("g:bufExplorerSplitBelow", &splitbelow) " Should horizontal splits be below or above current window? call s:Set("g:bufExplorerSplitOutPathName", 1) " Split out path and file name? call s:Set("g:bufExplorerSplitRight", &splitright) " Should vertical splits be on the right or left of current window? call s:Set("g:bufExplorerSplitVertSize", 0) " Height for a vertical split. If <=0, default Vim size is used. call s:Set("g:bufExplorerSplitHorzSize", 0) " Height for a horizontal split. If <=0, default Vim size is used. call s:Set("g:bufExplorerShowTerminal", 1) " Show terminal buffers? " Default key mapping {{{2 if !hasmapto('BufExplorer') && g:bufExplorerDisableDefaultKeyMapping == 0 nnoremap