6

I've been using Gvim for a while, but lately stopped using GUI options and went to just commands. :ls is used all the time, but I have a huge project and it is hard to see the file name quickly in the long path. I like how GVim displays buffers in the "Buffers" menu:

<file name>             <path from $HOME>

Is there a way to either

  1. Use built in functionality of vim to display just the filename in :ls
  2. Write a function that will use :ls and show just the filename.

I have a preference for the second solution because I don't want to totally override :ls.

Some quick notes: I am pretty comfortable with Vim commands (over a year using it as the only editor). But I've just started VimScript and this function would be a great first personal tool. If someone knows how to write a command for this, I would really appreciate a hint rather than a solution on where to start to write this function that would display just a file names of open buffers. I will promise to post what I write pretty quickly. :)

5
  • 2
    Buffer commands like :b can take partial filenames. Then use <tab> completion to deal with multiple matches. No more worrying about :ls. Jul 30, 2015 at 19:46
  • @Peter. That's very true and I use that all the time. The problem is though that I am dealing with OS code and file names get pretty ugly and pretty meaningless. :D Sometimes I want to see the filename I need to switch to and that's where GUI's "Buffers" menu is helpful. Jul 30, 2015 at 20:17
  • 1
    Why not simply use netrw? :Ex, see :help netrw.
    – romainl
    Jul 31, 2015 at 7:23
  • @romainl. Thank you for the response. I use :Vex and :Sex frequently as well. They serve a different purpose however: files are not in the same directory and can be pretty spread out. Jul 31, 2015 at 12:44
  • I agree with @Peter. I initially looked at implementing the accepted answer (didn't work, for me, Linux), but I then tried the :b <phrase> <tab> <Enter> approach, that works great! If you know the name of the buffer, type a unique part of the buffer name (e.g.) :b 2b <tab> or :b b. <tab> to expand :b /mnt/Vancouver/Programming/data/file2b.txt. Then hit the <Enter> key to open that buffer. It's a clean, "native" approach! :-) Mar 30, 2018 at 18:11

4 Answers 4

6

I came up with a solution which I think does what you want.

I know that you asked only for hints, so I'll write hints I would have given and then the final function. You'll be free to read it or not.

So here are my tips:

Creating a function

You said you were really new to vimscript, so here is a short reminder on functions.

They begins with function! NameOfTheFunc() (note the first capital letter on the name and the parentheses) and end with endfunction. You can put them in your .vimrc file to let vim source them at each start.

Getting the output of a function

If you want to get the output of a function to manipulate it, you can put it in a register thanks to the redir function. A use case using :ls (hey that's also a hint ;-) ) would be the following:

redir @n exec 'ls' redir END

This would put the result of :ls in the register @n.

Doing substitutions on the content of a register

I think you probably know the :s command and maybe the substitute() function. It is possible to use the latter to do substitution on the text of a register:

substitute(@n, {pattern}, {sub}, {flag})

Will apply the substitution on the register @n.

What about submatches?

I don't know how familiar you are with the substitute command so I'll also remind you that it is possible to create submatches. I think :h :substitute could interest you on this topic (I'll maybe edit this answer to put a link to a more relevant documentation). Basically it allows us to capture only a part of the pattern you're matching and reuse it.

The important point is that to use submatches with the substitute() function your call should look like this:

substitute(@n, {pattern}, '\=submatch(X)', {tags})

Where X is the number of the submatch you want to use. \= allows us to evaluate the content of the {sub} part instead of just using it literally.

And finally some regex

I will not make a complete explanation of regex here because it would be really long and I'm really not qualified to do that :-) This topic should be a start: :h regexp.

Also the function substitute() use magic parameter by default. So see :h magic.

Creating a command to feel "pro"

As we will create a function (let's call it ListBuffers()) that we will potentially call (very) often, we could create a custom ex command which is way faster to call than a function. To do so, we simply have to add the following line:

command! LS call ListBuffers()

With this line, typing :LS will be equivalent to typing call ListBuffers().

For the next step, I'll shamelessly stole a hint that I got from this VanLaser answer, many thanks to him. OP will probably not want to follow this step since you stated that you don't want to override :ls.

Our command :LS fills the same purpose as :ls, it is possible thanks to the alterCmd plugin to override the original :ls to use it instead our command.

With alterCmd installed, we can use the following line:

:AlterCommand ls LS

To replace the original :ls command with our custom one. This way, typing :ls will show the buffer list with the shortened names.

And then we have a function

Finally, with those different elements, I came up with a function which:

  • Gets the output of :ls in a register
  • Substitutes the full path to simply the name of the file
  • Put this result in a variable
  • Output this variable

function! ListBuffers()
    redir @n
    silent exec 'ls'
    redir END

    let list = substitute(@n, '"(\f*\)*(\f*)"' , '\=submatch(2)', "g")
    echo list
 
endfunction

Note the silent on line 3 which ensures that we execute ls but don't output its result on the screen so you have the list only once.

Note also the regex which can probably be improved. It creates 2 submatches. The first one contains \f*\\ which is as many file-name characters as possible followed by a literal \ and repeated as many time as possible. The second submatch only contains file-name characters and is the one used to replace the whole path.

Note: I created this regex on Windows because I don't have access to a Linux box right now, you might need to replace the literal \ by a literal / to make it work on Linux.

Here is the result of :ls on my machine:

enter image description here

And the result of my ListBuffers() function:

enter image description here

PS: Sorry for the long answer, I hope it helped ;-)

EDIT Carpetsmoker suggested using a variable instead of a register and I think he is particularly right. This way, using the function will not clobber the state of the register. So a better version of the function is:

function! ListBuffers()
    redir => ls_output
    silent exec 'ls'
    redir END

    let list = substitute(ls_output, '"(\f*\)*(\f*)"' , '\=submatch(2)',    "g")

    echo list 
 
endfunction

An alternative was to use getRegister() to save the state of @n and setRegister() to give it its previous state, but that's less effective than using a variable.

6
  • Using redir @n will clobber the @n register. Why not use a local variable with redir => ls_output? IMHO scripts/functions should never cause side-effects such as clobbering registers unless that's what they're designed to do. Aug 7, 2015 at 13:52
  • @Carpetsmoker you're right I updated my answer. Thanks for your comment! :-)
    – statox
    Aug 7, 2015 at 14:09
  • I want to apologize: I haven't forgotten about this. Life happened. Thank you so much statox and Carpetsmoker for writing this out. The redirection is very useful. I am catching up with vim script. joeytwiddle's solution is almost what I wanted. Vimscript is so great. I was using learnvimscriptthehardway.stevelosh.com for script lessons, but I like statox's tutorial. Just have to catch up with this. Aug 11, 2015 at 0:57
  • That's ok @Serge it happens to forget :-) LearVStheHardWay really is a good learning material (considering buying the book to support the author might be a good idea btw). This post will not replace hours of reading and months of practicing but I hope it will help you to begin is something! Also if you think that one answer actually answer your question feel free to accept it by clicking the green tick under the downvote button.
    – statox
    Aug 11, 2015 at 5:19
  • @statox I've made a rather massive edit here. It started with the spoiler markdown (which SE does support for code as well), and some grammar fixes, but I decided to replace the list with headers too. I hope I didn't mess up the function - I removed the numbers now that they're in a spoiler.
    – muru
    Aug 11, 2015 at 12:11
4

Here is my suggestion to get you started.

I haven't tested or debugged it, so that's your task. :)

function! s:MyBufList()
  set nomore
  let buf_count = bufnr("$")
  for i in range(1, buf_count)
    if getbufvar(l:i, '&buflisted') > 0
      let path = bufname(i)
      let filename = fnamemodify(path, ":t")
      let folder = fnamemodify(path, ":h")
      echo filename . "\t(" . folder . ")"
    endif
  endfor
  set more
endfunction

command! MBL call s:MyBufList()

nmap <silent> <Leader>b :MBL<CR>:b<Space>
2
  • Thank you joeytwiddle. This works really well, but as you said needs some work. The formatting is off, but i've been using this in the interim - the idea definitely works well (for me at least). As I mentioned above: I don't mean any disrespect by keeping silent - was busy for a while. Thank you and let me try all this. VimScript gets funky with some tasks. ;) Aug 11, 2015 at 1:00
  • Yes some padding to align columns would be nice. I have found using feedkeys in the mapping makes it behave slightly better: nmap <silent> <Leader>b :MBL<CR>:call feedkeys(':b ')<CR> Aug 28, 2015 at 10:26
3

You can also emulate :ls quite easily in VimL (and thus avoid :redir, the need to cut the line and analyse the filenames, ...)

On the way, you can filter and choose which filenames you wish to keep and also which part you want to display. However more work is required if you which to keep the attributes echoed by :ls.

Answers were given to a similar question (just filtering), on SO: https://stackoverflow.com/questions/2974192/how-can-i-pare-down-vims-buffer-list-to-only-include-active-buffers

I like the functional approach, other prefer plain loops:

:echo join(sort(map(filter(range(0, bufnr('$')), 'bufwinnr(v:val)>=0'), 'fnamemodify(bufname(v:val), ":t")'), 'u'), "\n")
2

To add on to the awesome answer from @statox above, here's what I had to do to get it to work on Linux. In addition to changing the appropriate \ to /, I had to escape the parentheses, resulting in this:

let list = substitute(ls_output, '"\(\f*/\)*\(\f*\)"', '\=submatch(2)', "g")

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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