Proposed tip Please edit this page to improve it, or add your comments below (do not use the discussion page).
One of the nice features of Vim is the ability to operate on text objects, for example di)
will delete text inside parentheses and dd
will delete a line (see :help text-objects). This tip provides a function to allow you to create your own such actions.
For example, I like to use \u
to open a URL. Using the mechanism below, \ui]
will open a URL contained within square brackets, \u$
from the cursor to the end of the line, \uu
an entire line, and v
...\u
the selected region in visual mode. With the helper functions below, setting this up is simple enough to be used for spur-of-the-moment needs such as reversing a string, computing MD5 sums, converting unicode characters to LaTeX, and so on.
The script[]
Put the following into your .vimrc
file. In the next section I'll give some usage examples, then I'll explain how it works.
" Adapted from unimpaired.vim by Tim Pope. function! s:DoAction(algorithm,type) " backup settings that we will change let sel_save = &selection let cb_save = &clipboard " make selection and clipboard work the way we need set selection=inclusive clipboard-=unnamed clipboard-=unnamedplus " backup the unnamed register, which we will be yanking into let reg_save = @@ " yank the relevant text, and also set the visual selection (which will be reused if the text " needs to be replaced) if a:type =~ '^\d\+$' " if type is a number, then select that many lines silent exe 'normal! V'.a:type.'$y' elseif a:type =~ '^.$' " if type is 'v', 'V', or '<C-V>' (i.e. 0x16) then reselect the visual region silent exe "normal! `<" . a:type . "`>y" elseif a:type == 'line' " line-based text motion silent exe "normal! '[V']y" elseif a:type == 'block' " block-based text motion silent exe "normal! `[\<C-V>`]y" else " char-based text motion silent exe "normal! `[v`]y" endif " call the user-defined function, passing it the contents of the unnamed register let repl = s:{a:algorithm}(@@) " if the function returned a value, then replace the text if type(repl) == 1 " put the replacement text into the unnamed register, and also set it to be a " characterwise, linewise, or blockwise selection, based upon the selection type of the " yank we did above call setreg('@', repl, getregtype('@')) " relect the visual region and paste normal! gvp endif " restore saved settings and register value let @@ = reg_save let &selection = sel_save let &clipboard = cb_save endfunction function! s:ActionOpfunc(type) return s:DoAction(s:encode_algorithm, a:type) endfunction function! s:ActionSetup(algorithm) let s:encode_algorithm = a:algorithm let &opfunc = matchstr(expand('<sfile>'), '<SNR>\d\+_').'ActionOpfunc' endfunction function! MapAction(algorithm, key) exe 'nnoremap <silent> <Plug>actions' .a:algorithm.' :<C-U>call <SID>ActionSetup("'.a:algorithm.'")<CR>g@' exe 'xnoremap <silent> <Plug>actions' .a:algorithm.' :<C-U>call <SID>DoAction("'.a:algorithm.'",visualmode())<CR>' exe 'nnoremap <silent> <Plug>actionsLine'.a:algorithm.' :<C-U>call <SID>DoAction("'.a:algorithm.'",v:count1)<CR>' exe 'nmap '.a:key.' <Plug>actions'.a:algorithm exe 'xmap '.a:key.' <Plug>actions'.a:algorithm exe 'nmap '.a:key.a:key[strlen(a:key)-1].' <Plug>actionsLine'.a:algorithm endfunction
Usage examples[]
An action is setup by calling MapAction
. The first argument should be the name of a function (which you have created) and the second argument should be the key to bind to. If the function returns a value, the text is replaced, otherwise the text is unchanged. For example, the following causes <leader>r
to reverse a string. The ReverseString
function simply takes the string as an argument and reverses it.
function! s:ReverseString(str) let out = join(reverse(split(a:str, '\zs')), '') " Remove a trailing newline that reverse() moved to the front. let out = substitute(out, '^\n', '', '') return out endfunction call MapAction('ReverseString', '<leader>r')
Now, typing <leader>ri)
will reverse a string within parentheses, <leader>rr
will reverse a line, and so on. It also works on visual selections.
The following allows opening URLs with <leader>ui]
, <leader>uu
, etc. Since the function doesn't return a value, the text is unchanged.
function! s:OpenUrl(str) silent execute "!firefox ".shellescape(a:str, 1) redraw! endfunction call MapAction('OpenUrl','<leader>u')
This one pipes the text through an external program. The selected text is piped into the given program and is replaced by that program's output.
function! s:ComputeMD5(str) let out = system('md5sum |cut -b 1-32', a:str) " Remove trailing newline. let out = substitute(out, '\n$', '', '') return out endfunction call MapAction('ComputeMD5','<leader>M')
How it works[]
Consider the ReverseString
example above. When MapAction
is called, the first three lines create internal mappings named <Plug>actionsReverseString
and <Plug>actionsLineReverseString
. The next three lines of MapAction
actually map these to your specified key sequence.
The first nmap
handles text objects such as <leader>ri)
. As you begin to type the sequence, <leader>r
, the mapping calls ActionSetup("ReverseString")
which in turn tells Vim that the ActionOpfunc
function should handle the subsequent motion command. The s:encode_algorithm
variable is what we use to remember what should be done once the motion command is received. It is set to ReverseString
. Finally, the key sequence g@
is sent, causing Vim to listen for a motion command (e.g. i)
). When you finish typing the motion command, ActionOpfunc
gets called, which defers to DoAction('ReverseString', ...)
. For more information on this sequence of events, see :help g@.
The xmap
in MapAction
is for visual mode. The second nmap
is for line mode, for example <leader>rr
or 3<leader>rr
.
In all cases, ultimately it is DoAction
that gets called. The algorithm
argument tells the user-defined function that should be called, and type
gives the type of the text object. We first set some configuration options (which are saved and restored later). The next task is to read the selected text into a variable. This is done by first yanking into the unnamed register (a backup copy of this register is saved in reg_save
and restored in the end). How the yank is done depends on the value of type
, however we always begin by making a visual selection since this can be reused later to replace the text if needed.
After the code is yanked into the unnamed register, we call the user-defined function (ReverseString
in this example), passing it the contents of the unnamed register, into which the text was yanked. If the function returned a value, then the selection is reselected with gv
and the new text pasted. Finally, the backed-up settings and the original content of the unnamed register are restored.
Related plugins[]
- TextTransform plugin handles all the creation of mappings (or custom commands); you just supply the transformation algorithm in the form of a Vim function! It is basically a generic utility plugin derived from the (static) unimpaired.vim plugin
- Toop plugin allows you to easily map behaviour to text objects, also fixing some issues with new lines results that this snippet don't cover.