How to Copy a Vim Buffer to the ClipboardNovember 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>
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.
<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.
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 = lines[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 (
'>). 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:
vnoremapnon-recursively maps the shortcut for visual mode, so it doesn’t conflict with the other binding
<c-u>erases the range that vim automatically pastes when starting an ex command with a selection
- It only needs one trailing
silent execdisables the “Press Enter…” prompt`
After adding this code to my
.vimrc and restarting vim, I can now copy and paste from visual mode. Here is the proof!
- X11 Selection Atoms
- Always use non-recursive variants; Losh, Learn Vimscript the Hard Way
- Range also makes vim pass the first and last line function arguments, but they’re not useful for character-wise selection. See
system()writes the lines to a file and sends that as stdin to xclip. See …
Tags: vim vimscript xclip x11 terminal linux