Wrapping selected text in Vim, easy... no?
A common want when editing code or prose, nay, necessity is to wrap some selected text with something else - it could be an opening / closing tag combo, or some kind of logging method or even some kind of formatting function. Here are some common examples:
- Wrap with delimiter characters
e.g.Hi!
to"Hi!"
- Wrap with a method
e.g."a string"
towrapped("a string")
- Wrap with a tag
e.g.Boo!
to<div>Boo!</div>
- Wrap with a method and do some pre/post cleanup
e.g.<?php echo $var ?>
to<?php Output::text($var) ?>
The common way of approaching the first three scenarios is by using a plugin such as surround
, sandwich
or UltiSnips
- but how about 4? In my particular case I also wanted to support wrapping with different methods on the Output
object and map them to different keys. So using plugins didn’t quite cut the hotdog relish. Time to roll up the sleeves.
Break it down (James Brown)
Ok, let’s decompose this into the following rough components:
- Capture the selected text into a variable
- Clear the text
- Do any clean up around the text
- Re-insert the text from the variable
- Map that all to a key or command
Capture the selected text into a variable
This sounds easy right? Just yank it into a register and copy that… somehow… is there already a built in method for that? Pretty sure there isn’t so you end up with something like:
function! GetVisualSelection()
" Get from the start of the visual selection, '<
let [line_start, column_start] = getpos("'<")[1:2]
" To the end of the visual selection, '>
let [line_end, column_end] = getpos("'>")[1:2]
let lines = getline(line_start, line_end)
if len(lines) == 0
return ''
endif
" Take into account the selection setting
let lines[-1] = lines[-1][: column_end - (&selection == 'inclusive' ? 1 : 2)]
let lines[0] = lines[0][column_start - 1:]
return join(lines, "\n")
endfunction
Insert text at current cursor
Let’s skip 2 and 3 (they are actually trivial, yay) and let’s ponder inserting text - again that should be built-in but you can’t simply put in something like exe "normal i" . text
as that’s not fool proof depending on what text
is. For this you’ll probably want something like:
function! InsertText(text)
let cur_line_num = line('.')
let cur_col_num = col('.')
let orig_line = getline('.')
let modified_line =
\ strpart(orig_line, 0, cur_col_num - 1)
\ . a:text
\ . strpart(orig_line, cur_col_num - 1)
" Replace the current line with the modified line.
call setline(cur_line_num, modified_line)
" Place cursor on the last character of the inserted text.
call cursor(cur_line_num, cur_col_num + strlen(a:text))
endfunction
Putting it all together
Let’s see what a full approach might look like:
function! GetVisualSelection()
" Get from the start of the visual selection, '<
let [line_start, column_start] = getpos("'<")[1:2]
" To the end of the visual selection, '>
let [line_end, column_end] = getpos("'>")[1:2]
let lines = getline(line_start, line_end)
if len(lines) == 0
return ''
endif
" Take into account the selection setting
let lines[-1] = lines[-1][: column_end - (&selection == 'inclusive' ? 1 : 2)]
let lines[0] = lines[0][column_start - 1:]
return join(lines, "\n")
endfunction
function! InsertText(text)
let cur_line_num = line('.')
let cur_col_num = col('.')
let orig_line = getline('.')
let modified_line =
\ strpart(orig_line, 0, cur_col_num - 1)
\ . a:text
\ . strpart(orig_line, cur_col_num - 1)
" Replace the current line with the modified line.
call setline(cur_line_num, modified_line)
" Place cursor on the last character of the inserted text.
call cursor(cur_line_num, cur_col_num + strlen(a:text))
endfunction
function! WrapIt(wrapper)
let sel = GetVisualSelection()
" Create the full end text we want
let text = '?php ' . a:wrapper . '(' . sel . ') ?'
" Delete everything inside the opening and closing angled brackets
normal di<
call InsertText(text)
endfunction
vmap <localleader>wt <esc>:call WrapIt('Output::text')<cr>
vmap <localleader>wi <esc>:call WrapIt('Output::id')<cr>
vmap <localleader>wa <esc>:call WrapIt('Output::attrib')<cr>
vmap <localleader>wh <esc>:call WrapIt('Output::html')<cr>
Personally I have the WrapIt
function and the mappings in a project scoped vimrc but the GetVisualSelection
and InsertText
functions are ripe for putting in a global autoload functions files.
So with that in place I can select some text using, say, viw
and hit ,it
(my localleader is a comma) et voilà - text wrapped and cleaned up!
- //Tags:
- //Posted: Jul 26, 2021