Vim Tips Wiki
Advertisement

Proposed tip Please edit this page to improve it, or add your comments below (do not use the discussion page).

Please use new tips to discuss whether this page should be a permanent tip, or whether it should be merged to an existing tip.
created August 11, 2013 · complexity basic · author Dstahlke · version 7.0

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.

Comments[]

Advertisement