Code

How to Copy a Vim Buffer to the Clipboard

November 23, 2021

I usually program with vim inside a tmux session. I’m a big fan of tmux, but the copy-and-paste feature is decidedly clunky. And my version of vim is compiled without clipboard support. What to do? I could install a vim with clipboard support, but surely this is just a small matter of programming.

Copy text into the clipboard

The clipboard is controlled by the windowing system; my system uses X11, so I use the xclip utility to copy text into the clipboard. If you’re using Wayland you’ll need to use a different utility, like wl-clipboard.

Xclip can read text from stdin:

echo 'foo' | xclip -sel c

Or a file:

xclip -sec c foo.txt

Of course this being Linux I need to tell xclip which clipboard to use¹. The option -sel c tells xclip to read the text into the selection buffer called “clipboard” which is similar to the Windows clipboard.

Translating to a vim shortcut

Vim’s write command has a useful variant² which writes the current buffer to the stdin of a command. All I have to do is hook it up to xclip:

:w !xclip -sel c

Press ENTER or type command to continue

I don’t always want to copy the whole file though. I can replicate the behavior of head by passing a range:

:1,.w !xclip -sel c

Press ENTER or type command to continue

This gives the range of line numbers 1 to the line the cursor is on (.). I can still copy the whole file, by first jumping the cursor to the bottom of the file with <SHIFT>G and then running the command.

I can map this to the shortcut <leader>h (h for head):

:nnoremap <leader>h :1,.w !xclip -sel c<CR><CR>

The option nnoremap tells vim to non-recursively³ map the shortcut for normal mode. The shortcut <leader>h means pressing the leader key⁴ and h at the same time. My leader key is bound to the default key: backslash.

That trailing <CR><CR> tells vim to type two newlines; the first submits the command, the second dismisses the “Press Enter…” prompt. Now when I press \h together, vim copies the range of text from the top of the current buffer to my cursor into my clipboard.

Copying selections

I also want to be able to copy the currently selected text into the clipboard. Vim has builtin support for passing ranges to functions, but the ranges are denoted per line. What if I only want to copy part of a line? Based on this Stack Overflow answer I ended up with:

function! RangeToClipboard() range
  let [line1, col1] = getpos("'<")[1:2]
  let [line2, col2] = getpos("'>")[1:2]
  let lines = getline(line1, line2)
  " handle incl/excl last char
  let lines[-1] = lines[-1][: col2 - (&sel == 'inclusive' ? 1 : 2)]
  let lines[0] = lines[0][col1 - 1:]
  silent exec system('xclip -sel c', lines)
endfunction

This function has an empty signature followed by the range argument, which I’m using to stop vim from calling the function once per line if its called with a range⁵. The function reads the selected text into lines and trims the first and last line according to the column numbers of the current selection ('< and '>). It uses the system() builtin to pass the lines variable to xclip⁶.

Now I can bind a shortcut to call it:

vnoremap <leader>h :<c-u>call RangeToClipboard()<CR>

The differences between this and the first binding:

After adding this code to my .vimrc and restarting vim, I can now copy and paste from visual mode. Here is the proof!

Notes

  1. X11 Selection Atoms
  2. See :help write_c
  3. Always use non-recursive variants; Losh, Learn Vimscript the Hard Way
  4. See :help leader
  5. Range also makes vim pass the first and last line function arguments, but they’re not useful for character-wise selection. See :help func-range
  6. Indirectly; system() writes the lines to a file and sends that as stdin to xclip. See … :help system 🤖

Tags: vim vimscript xclip x11 terminal linux