Я написал скрипт, который делает это:
function! WrapSelect (front)
"puts characters around the selected text.
let l:front = a:front
if (a:front == '[')
let l:back = ']'
elseif (a:front == '(')
let l:back = ')'
elseif (a:front == '{')
let l:back = '}'
elseif (a:front == '<')
let l:back = '>'
elseif (a:front =~ " ")
let l:split = split(a:front)
let l:back = l:split[1]
let l:front = l:split[0]
else
let l:back = a:front
endif
"execute: concat all these strings. '.' means "concat without spaces"
"norm means "run in normal mode and also be able to use \<C-x> characters"
"gv means "get the previous visual selection back up"
"c means "cut visual selection and go to insert mode"
"\<C-R> means "insert the contents of a register. in this case, the
"default register"
execute 'norm! gvc' . l:front. "\<C-R>\"" . l:back
endfunction
vnoremap <C-l> :<C-u>call WrapSelect(input('Wrapping? Give both (space separated) or just the first one: '))<cr>
Чтобы использовать, просто выделите что-нибудь, нажмите control l, а затем введите символ. Если это один из символов, о которых знает функция, он предоставит правильный завершающий символ. Если это не так, он будет использовать один и тот же символ для вставки с обеих сторон.
Surround.vim может сделать больше, чем просто это, но этого было достаточно для моих нужд.