Why I Never Use “:ls” In Vim

Sean
5 min readJul 17, 2021

Using buffers is a pretty fundamental part of using Vim properly and, to be honest, I avoided using them for a while when I started. They’re a weird concept if you’re used to using any gui program, where it’s more common to think in terms of files and tabs.

But the main problem I had to get over was the :ls command, which I still find cumbersome and un-intuitive. I used fzf to load my buffer list in the end, which has a preview and is way easier to use but I was still a bit miffed at myself for not being able to get over this fundamental bit of Vim functionality without a plugin.

fzf :Buffer command

Also there’s even a couple of things I don’t like about fzf’s buffer command. It takes too long to load, you can’t delete buffers using it or open more than one and I still have to refer to it far too often.

I started to address this problem by trying to make my own buffer display function that would open faster than fzf and would also allow me to delete buffers. It involved creating a new hidden buffer, pasting the contents of the buffer list (thanks to the :redir command) and restricting the cursor to up and down movements.

btw: pro tip, you can save the output of any command using either :redir

:redir => o | execute 'ls' | redir END | let buffers = o

…or the execute() function…

:let buffers = execute('ls')

Sometimes it’s better to use redir even though execute looks way cleaner.

For anyone interested in a bit of vim scripting, this is the function I wrote…

" The function's designed to take any command you want and turn it 
" into a menu list...
function! AnyList(cmd, ftype, cursorposition)
" If we're already in the buffer menu ignore...
if bufname("%") == "menu-list" | return | endif
" Save the buffer list into a variable called l:list...
silent! redir => o | execute 'silent ' . a:cmd | redir END | let l:list = o
" Set up the buffer-list buffer (this makes it hidden)...
let g:anylistbuff = bufnr('menu-list', 1)
call setbufvar(g:anylistbuff, "&buftype", "nofile")
call bufload('menu-list')
" Loop over each line in the buffer list and add
" it to our empty buffer...
let lnum = 0
for line in split(l:list, '\n')
let lnum += 1
call setbufline(g:anylistbuff, lnum, line)
endfor
" If the user leaves the list, delete it...
au! BufLeave menu-list execute g:anylistbuff . "bwipeout"
" Make a new split and add the new buffer...
execute "sbuffer" . g:anylistbuff

" Set a cursorline for the menu look and feel...
silent! set cursorline

" Give the buffer a filetype for color syntax
exe "silent! set filetype=" . a:ftype

" Depending on the list type you'll want the cursor to
" start in a specific position...
exe "norm " . a:cursorposition
" On "enter" open a buffer for the number
" under the cursor...
exe "map <silent> <buffer> <cr> :b " . expand("<cword>") . "<cr>"
" On "dd" delete the buffer number under the cursor...
exe "map <silent> <buffer> dd :bd " . expand("<cword>") . "<cr>V:g/./d\<cr>" . a:cursorposition
" Limit movement, you can still do "w" etc, this is just to make
" it a bit more like a menu...
map <buffer> h <NOP>
map <buffer> l <NOP>
map <buffer> <esc> <c-w>c
endfunction

It works pretty well in theory but needs a bit of extra work, I was finding that sometimes it would make buffers behave quite weirdly amongst other problems I can’t remember now, long story short I got bored and moved on because I found a better and simpler way to do what I wanted.

Instead I just use the statusline.

I don’t use Lightline or any fancy statusline plugin like a lot of people. I never actually found them that useful. Instead I set my own statusline with a few simple things like the current buffer and current file, also the percentage of the file as well.

If you run this command…

:set statusline=%p\ %b\ %f

Your statusline will change to show how far you’ve scrolled down the current file in percentage — that’s the %p — the current buffer number — that’s the %b — and the file name — the %f. The \ are to escape the spaces between each value.

To add the result of a function to the statusline wrap it in curly braces…

:set statusline=%{FugitiveHead()}

For anyone using the Fugitive plugin this will print your current git branch.

You can add anything to the statusline in plain text, :set statusline=boom will work, spaces must be escaped and anything following a % will be treated like a special value (check out :h statusline for the full list of special values, also to find the current value of your statusline at any time just run :set statusline).

I simply added my buffer list using a function and combine that with a few shortcuts for switching buffers and I almost never use fzf for dealing with buffers now.

function ListBuffers()
return join(
\map(
\getbufinfo({'buflisted':1}),
\{
\key, val ->
\val.bufnr == buffer_number() ?
\(len(val.name) > 0 ?
\fnamemodify(val.name, ":t") : '[No Name]') . '*'
\: (len(val.name) > 0 ? fnamemodify(val.name, ":t")
\: '[No Name]')
\}
\), ' • '
\)
endfunction
set statusline=%{ListBuffers()}

This maps the result of the getbufinfo function (which lists current buffers) and for each item it checks to see if it’s the current buffer, and adds a * if so, and the buffer has a name, if not it shows [No Name]. Then it joins the result into a string with a nice symbol in-between.

Then I map :bnext to <leader>bn, :bprevious to <leader>bp and :bdelete to <leader>bd.

Now I can see how many buffers I have, what order they’re in and which one I’m in. I don’t have to run any command to see them and I don’t have to think about which number matches which buffer.

This way I never use fzf for buffers and almost never use :ls. In fact, now when I do use :ls I actually find it very useful.

--

--