16

I have an image on the clipboard and I want to paste it into my document as the text [](img/image1.png).

Instead of first copying it to that directory, I want to do it with a single p key press in Vim. So it hooks p, checks if I am editing a Markdown file, saves the image from the clipboard to the location img/image1.png, and inserts [](img/image1.png) into the file.

Is this possible with Vim?

I use macOS and terminal Vim.

1
  • For clarity, is this "I have copied an image directly as if in, say, word" or "I have copied an image file in finder"? (I don't actually know if they are different...)
    – Joe
    Commented May 27, 2019 at 10:12

3 Answers 3

5

The usual terminal command for accessing the clipboard on Mac is pbpaste. However, this doesn't work for image data, so we have to resort to AppleScript:

nnoremap <silent> <buffer> p :call MarkdownClipboardImage()<cr>

function! MarkdownClipboardImage() abort
  " Create `img` directory if it doesn't exist
  let img_dir = getcwd() . '/img'
  if !isdirectory(img_dir)
    silent call mkdir(img_dir)
  endif

  " First find out what filename to use
  let index = 1
  let file_path = img_dir . "/image" . index . ".png"
  while filereadable(file_path)
    let index = index + 1
    let file_path = img_dir . "/image" . index . ".png"
  endwhile

  let clip_command = 'osascript'
  let clip_command .= ' -e "set png_data to the clipboard as «class PNGf»"'
  let clip_command .= ' -e "set referenceNumber to open for access POSIX path of'
  let clip_command .= ' (POSIX file \"' . file_path . '\") with write permission"'
  let clip_command .= ' -e "write png_data to referenceNumber"'

  silent call system(clip_command)
  if v:shell_error == 1
    normal! p
  else
    execute "normal! i[](" . file_path . ")"
  endif
endfunction

Add the code above to ~/.vim/after/ftplugin/markdown.vim.

This saves the clipboard data to the file "img/image1.png" if it contains PNG data. It then just uses the :normal! to insert the desired markdown link.

If the clipboard doesn't contain PNG data (or if we're not in a Markdown file), it performs a regular p command.

If I were using this, I'd tidy it up to support further image types and to leave the cursor in a more helpful position, but this works well as a proof of concept.

(I'd also use a <leader>p mapping as suggested by Karl Yngve Lervåg — this seems like functionality I'd prefer to invoke specifically — but that's not what you asked for).

Further reading

2
  • Nice, I've updated my answer with a link to yours, since your solution actually answers the MacOS requirement. Commented Nov 8, 2017 at 7:37
  • 1
    @KarlYngveLervåg Thanks, that's very kind of you. I wouldn't even have written my answer if it wasn't for yours: you inspired me to find a native Mac solution!
    – Rich
    Commented Nov 8, 2017 at 9:41
9

The following solution should work on most Linux systems. It requires a relatively updated xclip tool. For a solution that works on MacOS/OSX, see @Rich's answer.


I've proposed a solution that maps <leader>p instead of remapping p, since I think this is a much saner approach. The function SaveFile will inspect the clipboard with xclip. If there is an image file, it will be saved to img/imageN.<ext>, unless the file already exists. It will also populate the * register with the [](img/...) text and then paste it.

nnoremap <silent> <leader>p :call SaveFile()<cr>

function! SaveFile() abort
  let targets = filter(
        \ systemlist('xclip -selection clipboard -t TARGETS -o'),
        \ 'v:val =~# ''image''')
  if empty(targets) | return | endif

  let outdir = expand('%:p:h') . '/img'
  if !isdirectory(outdir)
    call mkdir(outdir)
  endif

  let mimetype = targets[0]
  let extension = split(mimetype, '/')[-1]
  let tmpfile = outdir . '/savefile_tmp.' . extension
  call system(printf('xclip -selection clipboard -t %s -o > %s',
        \ mimetype, tmpfile))

  let cnt = 0
  let filename = outdir . '/image' . cnt . '.' . extension
  while filereadable(filename)
    call system('diff ' . tmpfile . ' ' . filename)
    if !v:shell_error
      call delete(tmpfile)
      break
    endif

    let cnt += 1
    let filename = outdir . '/image' . cnt . '.' . extension
  endwhile

  if filereadable(tmpfile)
    call rename(tmpfile, filename)
  endif

  let @* = '[](' . fnamemodify(filename, ':.') . ')'
  normal! "*p
endfunction
2
  • This solution fails silently for me :( Commented Nov 11, 2017 at 8:37
  • My guess is that you have an old version of xclip. You should try to do the xclip commands in your terminal. Commented Nov 12, 2017 at 10:05
6

Based on answers above I've created a Plugin for vim solving my problem for macos and possibly for linux. Hope you'll find this plugin useful. I also appreciate any contributions and feature requests.

1
  • I don't think I ever saw this before. Thanks for the credit in your README! I started writing a plugin, too, but I never got around to implementing all the features I had planned.
    – Rich
    Commented Jul 11, 2019 at 20:20

Your Answer

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

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