I finally figured it out. The keys are:
- the marks for the starting (
'<
) and ending ('>
) positions of the visual selection;¹
- functions
getpos()
and setpos()
to manipulate marks from vimscript.
- (the normal-mode command
gv
to reselect the active visual selection.)
Mixing these ingredients is less straightforward than one might expect, because of oddities of Vim. Also, column numbers are bytewise, so this will break with multibyte characters.
The (much expanded) code looks like this.
" These functions will not work as intended in the presence of multibyte
" characters.
fun! ExpandVisualBefore(by_lines, by_cols)
let [start_buf, start_line, start_col, start_off] = getpos("'<")
let end_pos = getpos("'>")
if start_off != 0
throw 'function ExpandVisualBefore(): ''virtualedit'' is not supported'
endif
" Note: “:help getpos()” says that the column of '< should be 0 when in
" visual-line mode, but my tests indicate that it is set to 1 instead.
" the column of '> should be set to a “large number”, which my tests confirm.
if end_pos[2] >= 2147483647 && a:by_cols != 0
echohl WarningMsg
echo 'function ExpandVisualBefore(): cannot add or remove columns in visual-line mode'
echohl None
endif
if setpos("'<", [start_buf, start_line-a:by_lines, start_col-a:by_cols, 0]) == -1
throw 'function ExpandVisualBefore(): setpos() failed'
endif
" Note: For some obscure reason, when the cursor is at the start of the
" selection, changing '< apparently sets '> to the former value of '<. Below,
" we fix that by restoring '> to its initial value.
" Under the same circumstances, the cursor is moved to the end of the
" selection, we should fix that as well…
if setpos("'>", end_pos) == -1
throw 'function ExpandVisualAfter(): setpos() failed'
endif
endfun
fun! ExpandVisualAfter(by_lines, by_cols)
let start_pos = getpos("'<")
let [end_buf, end_line, end_col, end_off] = getpos("'>")
if end_off != 0
throw 'function ExpandVisualAfter(): ''virtualedit'' is not supported'
endif
if end_col >= 2147483647 && a:by_cols != 0
echohl WarningMsg
echo 'function ExpandVisualAfter(): cannot add or remove columns in visual-line mode'
echohl None
endif
if setpos("'>", [end_buf, end_line+a:by_lines, end_col+a:by_cols, 0]) == -1
throw 'function ExpandVisualAfter(): setpos() failed'
endif
" Note: For some obscure reason, when the cursor is at the start of the
" selection, changing '> apparently sets '< to the former value of '>. Below,
" we fix that by restoring '< to its initial value.
" Under the same circumstances, the cursor is moved to the end of the
" selection, we should fix that as well…
if setpos("'<", start_pos) == -1
throw 'function ExpandVisualAfter(): setpos() failed'
endif
endfun
vnoremap <silent> \- :call ExpandVisualBefore(0,-v:count1)<CR>gv
vnoremap <silent> \+ :call ExpandVisualBefore(0,v:count1)<CR>gv
vnoremap <silent> - :call ExpandVisualAfter(0,-v:count1)<CR>gv
vnoremap <silent> + :call ExpandVisualAfter(0,v:count1)<CR>gv
Then, in visual mode, typing 3+
(resp. 3\+
) extends the selection by three characters to the right (resp. left); typing 3-
(resp. 3\-
) shrinks it by three characters to the right (resp. left).
¹ Notation are inconsistent here: in range and movements, `<
and `>
are the starting and ending positions of the selection, whereas '<
and '>
are linewise (they only mean the line where the selection begins or ends); but as far as getpos()
/ setpos()
is concerned, '<
and '>
must be used to manipulate the full positions.