1986 lines
66 KiB
VimL
1986 lines
66 KiB
VimL
"============================================================================
|
|
" 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
|
|
"
|
|
" <Leader>be - Opens BufExplorer
|
|
" <Leader>bt - Toggles BufExplorer open or closed
|
|
" <Leader>bs - Opens horizontally split window BufExplorer
|
|
" <Leader>bv - Opens vertically split window BufExplorer
|
|
"
|
|
" Or you can override the defaults and define your own mapping
|
|
" in your vimrc file, for example:
|
|
"
|
|
" nnoremap <silent> <F11> :BufExplorer<CR>
|
|
" nnoremap <silent> <s-F11> :ToggleBufExplorer<CR>
|
|
" nnoremap <silent> <m-F11> :BufExplorerHorizontalSplit<CR>
|
|
" nnoremap <silent> <c-F11> :BufExplorerVerticalSplit<CR>
|
|
"
|
|
" 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,<SID>ActionArgs
|
|
\ BufExplorer :call BufExplorer(<f-args>)
|
|
command! -nargs=? -complete=custom,<SID>ActionArgs
|
|
\ ToggleBufExplorer :call ToggleBufExplorer(<f-args>)
|
|
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("<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
|
|
|
|
" DoBufEnter {{{2
|
|
function! s:DoBufEnter()
|
|
let bufNbr = str2nr(expand("<abuf>"))
|
|
let tabNbr = tabpagenr()
|
|
let tabId = s:MRUEnsureTabId(tabNbr)
|
|
call s:MRUAddBufTab(bufNbr, tabId)
|
|
endfunction
|
|
|
|
" DoBufDelete {{{2
|
|
function! s:DoBufDelete()
|
|
let bufNbr = str2nr(expand("<abuf>"))
|
|
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 <silent> <buffer> <Plug>(BufExplorer_BufferDelete) :call <SID>RemoveBuffer("delete")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_BufferDeleteForced) :call <SID>RemoveBuffer("force_delete")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_BufferWipe) :call <SID>RemoveBuffer("wipe")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_BufferWipeForced) :call <SID>RemoveBuffer("force_wipe")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_Close) :call <SID>Close()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_OpenBuffer) :call <SID>SelectBuffer()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_OpenBufferAsk) :call <SID>SelectBuffer("ask")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_OpenBufferOriginalWindow) :call <SID>SelectBuffer("original_window")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_OpenBufferSplitAbove) :call <SID>SelectBuffer("split", "st")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_OpenBufferSplitBelow) :call <SID>SelectBuffer("split", "sb")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_OpenBufferSplitLeft) :call <SID>SelectBuffer("split", "vl")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_OpenBufferSplitRight) :call <SID>SelectBuffer("split", "vr")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_OpenBufferTab) :call <SID>SelectBuffer("tab")<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_SortByNext) :call <SID>SortSelect()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_SortByPrev) :call <SID>ReverseSortSelect()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_ToggleFindActive) :call <SID>ToggleFindActive()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_ToggleHelp) :call <SID>ToggleHelp()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_ToggleOnlyOneTab) :call <SID>ToggleOnlyOneTab()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_ToggleReverseSort) :call <SID>SortReverse()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_ToggleShowRelativePath) :call <SID>ToggleShowRelativePath()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_ToggleShowTabBuffer) :call <SID>ToggleShowTabBuffer()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_ToggleShowTerminal) :call <SID>ToggleShowTerminal()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_ToggleShowUnlisted) :call <SID>ToggleShowUnlisted()<CR>
|
|
nnoremap <silent> <buffer> <Plug>(BufExplorer_ToggleSplitOutPathName) :call <SID>ToggleSplitOutPathName()<CR>
|
|
|
|
if exists("b:displayMode") && b:displayMode == "winmanager"
|
|
nnoremap <buffer> <silent> <tab> :call <SID>SelectBuffer()<CR>
|
|
endif
|
|
|
|
nmap <nowait> <buffer> <2-leftmouse> <Plug>(BufExplorer_OpenBuffer)
|
|
nmap <nowait> <buffer> <CR> <Plug>(BufExplorer_OpenBuffer)
|
|
nmap <nowait> <buffer> <F1> <Plug>(BufExplorer_ToggleHelp)
|
|
nmap <nowait> <buffer> <s-cr> <Plug>(BufExplorer_OpenBufferTab)
|
|
nmap <nowait> <buffer> a <Plug>(BufExplorer_ToggleFindActive)
|
|
nmap <nowait> <buffer> b <Plug>(BufExplorer_OpenBufferAsk)
|
|
nmap <nowait> <buffer> B <Plug>(BufExplorer_ToggleOnlyOneTab)
|
|
nmap <nowait> <buffer> d <Plug>(BufExplorer_BufferDelete)
|
|
nmap <nowait> <buffer> D <Plug>(BufExplorer_BufferWipe)
|
|
nmap <nowait> <buffer> f <Plug>(BufExplorer_OpenBufferSplitBelow)
|
|
nmap <nowait> <buffer> F <Plug>(BufExplorer_OpenBufferSplitAbove)
|
|
nmap <nowait> <buffer> o <Plug>(BufExplorer_OpenBuffer)
|
|
nmap <nowait> <buffer> O <Plug>(BufExplorer_OpenBufferOriginalWindow)
|
|
nmap <nowait> <buffer> p <Plug>(BufExplorer_ToggleSplitOutPathName)
|
|
nmap <nowait> <buffer> q <Plug>(BufExplorer_Close)
|
|
nmap <nowait> <buffer> r <Plug>(BufExplorer_ToggleReverseSort)
|
|
nmap <nowait> <buffer> R <Plug>(BufExplorer_ToggleShowRelativePath)
|
|
nmap <nowait> <buffer> s <Plug>(BufExplorer_SortByNext)
|
|
nmap <nowait> <buffer> S <Plug>(BufExplorer_SortByPrev)
|
|
nmap <nowait> <buffer> t <Plug>(BufExplorer_OpenBufferTab)
|
|
nmap <nowait> <buffer> T <Plug>(BufExplorer_ToggleShowTabBuffer)
|
|
nmap <nowait> <buffer> u <Plug>(BufExplorer_ToggleShowUnlisted)
|
|
nmap <nowait> <buffer> v <Plug>(BufExplorer_OpenBufferSplitRight)
|
|
nmap <nowait> <buffer> V <Plug>(BufExplorer_OpenBufferSplitLeft)
|
|
nmap <nowait> <buffer> X <Plug>(BufExplorer_ToggleShowTerminal)
|
|
|
|
for k in ["G", "n", "N", "L", "M", "H"]
|
|
execute "nnoremap <buffer> <silent>" k ":keepjumps normal!" k."<CR>"
|
|
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, '" <F1> : toggle this help')
|
|
call add(header, '" <enter> or o or Mouse-Double-Click : open buffer under cursor')
|
|
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 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 <F1> 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("<SID>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 <script> <silent> <unique> <Leader>be :BufExplorer<CR>
|
|
endif
|
|
|
|
if !hasmapto('ToggleBufExplorer') && g:bufExplorerDisableDefaultKeyMapping == 0
|
|
nnoremap <script> <silent> <unique> <Leader>bt :ToggleBufExplorer<CR>
|
|
endif
|
|
|
|
if !hasmapto('BufExplorerHorizontalSplit') && g:bufExplorerDisableDefaultKeyMapping == 0
|
|
nnoremap <script> <silent> <unique> <Leader>bs :BufExplorerHorizontalSplit<CR>
|
|
endif
|
|
|
|
if !hasmapto('BufExplorerVerticalSplit') && g:bufExplorerDisableDefaultKeyMapping == 0
|
|
nnoremap <script> <silent> <unique> <Leader>bv :BufExplorerVerticalSplit<CR>
|
|
endif
|
|
|
|
" vim:ft=vim foldmethod=marker sw=4
|