I'd like to use ls command to show opened buffers.

I'd like to make the output of this command as simple as possible. I'd like to see only the buffers' names, numbers and status (#,%,+,..).

I don't need to see the first line in the result that shows the full path of the current file and also don't need to see the buffers' pathes and the current line number in each buffer. Only show path if two buffers have the same name and come from different directories.

My idea behind this is that I'd like to achieve this with using vim's builtin features. Is there a way to make ls output that simple?


Try this:

command -bar -bang Ls call s:ls(<bang>0)

function s:ls(bang) abort
    let bufnrs = range(1, bufnr('$'))
    call filter(bufnrs, a:bang ? {_, v -> bufexists(v)} : {_, v -> buflisted(v)})
    let bufnames = copy(bufnrs)
        \ ->map({_, v -> bufname(v)->fnamemodify(':t')})
    let uniq_flags = copy(bufnames)->map({_, v -> count(bufnames, v) == 1})
    let items = map(bufnrs, {i, v -> #{
        \ bufnr: v,
        \ text: s:gettext(v, uniq_flags[i]),
        \ }})
    call setloclist(0, [], ' ', #{
        \ items: items,
        \ title: 'ls' .. (a:bang ? '!' : ''),
        \ quickfixtextfunc: 's:quickfixtextfunc',
        \ })
    nmap <buffer><nowait><expr><silent> <cr> <sid>Cr()

function s:Cr()
    if w:quickfix_title =~# '^ls!\=$'
        let locid = win_getid()
        return "\<c-w>\<cr>\<plug>(close-location-window)" .. locid .. "\<cr>\<plug>(verticalize)"
        return "\<c-w>\<cr>\<plug>(verticalize)"
nnoremap <plug>(close-location-window) :<c-u>call <sid>CloseLocationWindow()<cr>
nnoremap <plug>(verticalize) :<c-u>wincmd L<cr>
function s:CloseLocationWindow()
    let locid = input('')->str2nr()
    call win_execute(locid, 'close')

function s:gettext(v, is_uniq) abort
    let format = ' %*d%s%s%s%s%s %s'
    let bufnr = [bufnr('$')->len(), a:v]
    let buflisted = !buflisted(a:v) ? 'u': ' '
    let cur_or_alt = a:v == bufnr('%') ? '%' : a:v == bufnr('#') ? '#' : ' '
    let active_or_hidden = win_findbuf(a:v)->empty() ? 'h' : 'a'
    let modifiable = getbufvar(a:v, '&ma', 0) ? ' ' : '-'
    let modified = getbufvar(a:v, '&mod', 0) ? '+' : ' '
    let bufname = bufname(a:v)->empty()
        \ ?  '[No Name]'
        \ :   bufname(a:v)->fnamemodify(a:is_uniq ? ':t' : ':p')
    return call('printf', [format]
        \ + bufnr
        \ + [buflisted, cur_or_alt, active_or_hidden, modifiable, modified, bufname])

function s:quickfixtextfunc(info) abort
    let items = getloclist(a:info.winid, #{id : a:info.id, items : 1}).items
    let l = []
    for idx in range(a:info.start_idx - 1, a:info.end_idx - 1)
        call add(l, items[idx].text)
    return l

It installs a :Ls command which should display a simplified buffer list in the location window. Press Enter on an entry to jump to the buffer.

Just like with the builtin :ls, append a bang (:Ls!) to include the unlisted buffers.

This requires a recent Vim version which includes these patches:

  • 8.2.0869 it is not possible to customize the quickfix window contents
  • 8.2.0933 'quickfixtextfunc' does not get window ID of location list
  • 8.2.0959 using 'quickfixtextfunc' is a bit slow

To gain in efficiency, readability and reliability, you could rewrite the code in Vim9 script:


command -bar -bang Ls Ls(<bang>0)

def Ls(bang: any)
    var bufnrs = range(1, bufnr('$'))
    filter(bufnrs, bang ? (_, v) => bufexists(v) : (_, v) => buflisted(v))
    var bufnames = copy(bufnrs)
        ->mapnew((_, v) => bufname(v)->fnamemodify(':t'))
    var uniq_flags = copy(bufnames)
        ->mapnew((_, v) => count(bufnames, v) == 1)
    var items = mapnew(bufnrs, (i, v) => ({
        bufnr: v,
        text: Gettext(v, uniq_flags[i]),
    setloclist(0, [], ' ', {
        items: items,
        title: 'ls' .. (bang ? '!' : ''),
        quickfixtextfunc: 'Quickfixtextfunc',
    nmap <buffer><nowait><expr><silent> <cr> <sid>Cr()

def Cr(): string
    if w:quickfix_title =~ '^ls!\=$'
        var locid = win_getid()
        return "\<c-w>\<cr>\<plug>(close-location-window)" .. locid
            .. "\<cr>\<plug>(verticalize)"
        return "\<c-w>\<cr>\<plug>(verticalize)"
nnoremap <plug>(close-location-window) :<c-u>call <sid>CloseLocationWindow()<cr>
nnoremap <plug>(verticalize) :<c-u>wincmd L<cr>
def CloseLocationWindow()
    var locid = input('')->str2nr()
    win_execute(locid, 'close')

def Gettext(v: number, is_uniq: bool): string
    var format = ' %*d%s%s%s%s%s %s'
    var bufnr = [bufnr('$')->len(), v]
    var buflisted = !buflisted(v) ? 'u' : ' '
    var cur_or_alt = v == bufnr('%') ? '%' : v == bufnr('#') ? '#' : ' '
    var active_or_hidden = win_findbuf(v)->empty() ? 'h' : 'a'
    var modifiable = getbufvar(v, '&ma', 0) ? ' ' : '-'
    var modified = getbufvar(v, '&mod', 0) ? '+' : ' '
    var bufname = bufname(v)->empty()
        ?  '[No Name]'
        :   bufname(v)->fnamemodify(is_uniq ? ':t' : ':p')
    return call('printf', [format]
        + bufnr
        + [buflisted, cur_or_alt, active_or_hidden,
           modifiable, modified, bufname])

def Quickfixtextfunc(info: dict<number>): list<any>
    var items = getloclist(info.winid, {id: info.id, items: 1}).items
    var l = []
    for idx in range(info.start_idx - 1, info.end_idx - 1)
        add(l, items[idx].text)
    return l

This requires an even more recent Vim version. It works on 8.2.2332.

You don't have to write the code in your vimrc, just put it in ~/.vim/plugin/myls.vim. Test it; keep it as long as you find it useful; remove it once you find something better.

For more info, see:

  • Thanks for your answer. This is what I was looking for. However, I think there are two improvements that will make it perfect. First is to close the quickfix when hitting enter and openning the buffer. Second is to be able to open the buffer in vertical split. – Salahuddin Ahmed Sep 8 '20 at 7:31
  • I found this answer stackoverflow.com/a/21326968/932786 that makes quickfix close after opening one of the files but I think it is better to close it only if it is viewing the list of buffers and not in any case. – Salahuddin Ahmed Sep 8 '20 at 7:41
  • I also found this answer to open a file in vertical split stackoverflow.com/a/16743676/932786 – Salahuddin Ahmed Sep 8 '20 at 7:59

In my library plugin, I have a :Project ls feature that lists only the files that belongs to the current project as :ls would have listed them.

The related (GPL w/ exception) code is the following:

function! s:As_ls(bid) abort " {{{2
  let name = bufname(a:bid)
  if empty(name)
    let name = 'Used to be known as: '.lh#project#__buffer(a:bid)
  return printf('%3d%s %s'
        \ , a:bid
        \ , (buflisted(a:bid) ? ' ' : 'u')
        \ . (bufnr('%') == a:bid ? '%' : bufnr('#') == a:bid ? '#' : ' ')
        \ . (! bufloaded(a:bid) ? ' ' : bufwinnr(a:bid)<0 ? 'h' : 'a')
        \ . (! getbufvar(a:bid, "&modifiable") ? '-' : getbufvar(a:bid, "&readonly") ? '=' : ' ')
        \ . (getbufvar(a:bid, "&modified") ? '+' : ' ')
        \ , '"'.name.'"')

function! s:ls_project(prj) abort " {{{2
  if lh#option#is_unset(a:prj)
    echo '(no project specified!)'
  let lines = map(copy(a:prj.buffers), 's:As_ls(v:val)')
  echo "Buffer list of ".get(a:prj, 'name', '(unnamed)')." project:"
  echo join(lines, "\n")

Instead of a:prj.buffers, you would have the list of buffers in range(1, bufnr('$')). It would then need to be filtered to keep only the existing buffers (filter(range(1, bufnr('$')), 'bufexists(v:val)')).

Then you could wrap the call to bufname() with fnamemodify() to display exactly what you need.

IOW, the s:ls_project function shall become something like this (untested).

function! s:ls() abort " {{{2
  let buffers = filter(range(1, bufnr('$')), 'bufexists(v:val)')
  let lines = map(copy(buffers), 's:As_ls(v:val)')
  echo "Buffer list:"
  echo join(lines, "\n")

command! -nargs=0 LS call s:ls()
" or with a mapping
nnoremap <leader>r :<c-u>call <sid>ls()<cr>

And then you'll just have to play with s:As_ls() code to make it produce what you wish to see. a:bid is the formal parameter holding the buffer id, and to know what each function does, see for instance :h buflisted(), and so on.

  • 1
    thanks @Luc for your answer. Unfortunately I have no such deep knowledge in vim scripting. I've copied your two functions in my vimrc and then I assum I need to map key-combination to call the s:ls_project() function, something like nmap ,r :call s:ls_project(). Is it correct? I don't know what should I give as argumen to the function. Could you please clarify in simple way what should I exactly do to get it to work? Thanks – Salahuddin Ahmed Sep 7 '20 at 11:52
  • 1
    The function is a little bit more complex to call like that as it expect a precise parameter and a few other things from my library plugin. I've edited my anwser – Luc Hermitte Sep 7 '20 at 12:20
  • +1 and thanks for [updating] your answer but I couldn't bring it to work. – Salahuddin Ahmed Sep 7 '20 at 13:14
  • What do you mean? You have an error? or not the expected output? – Luc Hermitte Sep 7 '20 at 13:21
  • 1
    My fault. I didn't know that I have to install your library. Now it works and I got the list of the buffers. I think I have now to play with let name = bufname(a:bid) to make it return the file name without its path and only return the path if there is a conflict with other buffer(s). I'm not sure if this is possible. But generally, my main idea was that I wanted to use pure vim without any plugin/library to reach what I want. – Salahuddin Ahmed Sep 7 '20 at 13:51

Is there a way to make ls output that simple?

Not natively... Vim doesn't really support options to customize or control output of the :ls command.

Having said that, you can write your own user-defined command that implements what you want.

You can either use execute('ls') to capture the string output of the original command and then post-process it (possibly using regular expressions and the substitute() function), or you can query the individual buffers one by one to assemble the information about them yourself. You can use bufnr('$') to find the highest buffer number (so you know where to stop looking for them.)

A user command needs to start with an uppercase, though, so you can only do :Ls or :LS. But one thing you can do is use a :cnoremap to capture use of :ls in the Ex command-line and replace it with your alternate implementation. But be careful that :cnoremap is typically too broad, it applies anywhere in the command-line and also affects search with /, prompting user with input(), etc. So usually you want to use an <expr> mapping that performs several checks before doing an actual replacement. A nice technique is to do a :cnoremap <expr> <CR> that only triggers when you're about to submit a command, then you can inspect the whole command and do a replacement if needed.

So, yeah, it's doable... But if you want my opinion, it's way too much work. YMMV.

  • 2
    But if you want my opinion, it's way too much work You're right. But, well... I already did it ^^' – Luc Hermitte Sep 7 '20 at 11:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.