What is the best way to bind key, to go "past the last delimiter. I want to bind, for example <leader>, to behave like this:

  • "foo bar| baz" -> key press -> "foo bar baz",|
  • (arg1, arg2, |arg3) -> key press -> (arg1, arg2, arg3),|

And same for other delimiter i choose. Is there any plugin for this?

For now my solution is this function (well, key binding is easy):

    function! MovePastDelimiter()
        let parantheses = { '('  : ')', '['  : ']', '{'  : '}', '<'  : '>' }
        let quotes_and_etc = ['"', '|', '`', '''']
        let regex = "[" . join(keys(parantheses),"") . join(quotes_and_etc,"") . "]"
        let match = searchpos(regex, 'bn')
        if match != [0, 0]
            let current_delimiter = getline(match[0])[match[1]-1]
            if has_key(parantheses, current_delimiter)
                if !searchpair(current_delimiter, '', parantheses[current_delimiter])
                    " this is stupid, but searchpair will
                    " work only with \[, not just [
                    call searchpair('\' . current_delimiter, '', '\' . parantheses[current_delimiter])

                call search(current_delimiter)

But maybe there is a better way?

  • If you want to use searchpair() on more than one delimiter at a time, there are two options: 1- Use [([{<"] and [)\]}>"], but it may give odd result on unbalanced pairs. 2- Run as many searchpairpos('{', '}', 'n') as required (every time with a different brackets pair), and see which one give the closest result. Use that one to jump past what you're looking for. – Luc Hermitte Nov 7 '16 at 16:04
  • BTW, I see that searchpair*() function don't work with equal {start} and {end} parameters. This means that while you could use it to detect the end of the current outer block, if won't work to detect the end of the current string. – Luc Hermitte Nov 7 '16 at 16:55
  • @LucHermitte yeah, just found out it myself, it will work for ({[< but not for single and double quotes. Too bad. – coredump Nov 7 '16 at 17:06

This was a tough one as:

  • I couldn't get getpos('``<') to work
  • searchpair*() don't work when the opening and the closing signs are not identical. Here, two cases:
    • with strings, va' and va" can be used
    • with other possible pairs like _ and * in markdown, * and | in vim help files, we have to use search() twice
  • searchpairpos() returns a byte offset and not a column position, this means that we need to count characters in UTF-8 documents -- as I didn't want to use setpos() but instead move the cursor in a redoable way
  • sometimes we have pairs like angle-brackets that can be used for other things (like ordered comparisons), and they can introduce new jump points (I have no generic solution for this one).

Note that I've integrated your idea in lh-brackets where I'm maintaining the list of registered pairs automatically.

I've ended up with two functions: one that searches for the compatible pairs (used to define block), and one called from various mappings to actually move the cursor. NB: I won't reproduce the code from lh-vim-lib functions used:

" Function: s:outer_blocks() {{{2
function! s:outer_blocks() abort
  let crt_pairs = s:GetPairs(0) " global pairs
  call extend(copy(crt_pairs), s:GetPairs(1), 'force') " buffer-local ones
  let matches = {}
  for p in crt_pairs
    if p[0] != p[1] " searchpos doesn't work in that case
      let pos = searchpairpos(p[0], '', p[1], 'cWn', "lh#syntax#is_a_comment('.')")
      call s:Verbose('Testing searchpos(%1) -> %2', p, pos)
    elseif p[0] =~ '["'']'
      " Stuff which can be checked with vi', vi"
      let crt_pos = getpos('.')
      let cleanup = lh#on#exit()
            \.register('call setpos(".", '.string(crt_pos).')')
        let @a = ''
        silent! exe 'normal! "aya'.p[0]
        " 2 chars are to be expected for open and close
        if lh#encoding#strlen(@a) >= lh#encoding#strlen(p[0].p[1])
          " In two steps because it may fail
          " -- it shouldn't though thanks to len(@a) >= 2 * len(open)
          exe 'normal! v'
          silent! exe 'normal! a'.p[0]
          silent! exe "normal! \<esc>"
          let pos = getpos('.')[1:2]
          let pos = [0,0]
        if 0
          " getpos doesn't seem to work...
          let pos = getpos('`>')[1:2]
          if getpos('`<')[1:2] == pos
            let pos = [0,0]
        call s:Verbose('Testing va%1 -> %2 - %3', p[0], pos, @a)
        call cleanup.finalize()
      let pos = searchpos(p[0], 'cWnb', 'lh#syntax#is_a_comment(".")')
      if  pos != [0,0]
        let pos = searchpos(p[1], 'cWn', 'lh#syntax#is_a_comment(".")')
      call s:Verbose('Testing /%1 -> %2', p[0], pos)
    if pos != [0,0]
      let matches[p[0]] = pos
  call s:Verbose('Containing bracket pairs: %1', matches)
  return matches

" Function: lh#brackets#jump_outside(param) {{{2
" In this flavour, we don't expect to be just before the current closing
" character. Instead, search for the next character that closes the current
" scope.
function! lh#brackets#jump_outside(param) abort
  let mode      = get(a:param, 'mode')

  let matches = s:outer_blocks()
  if empty(matches)
    call s:Verbose('The cursor doesn''t belong to any block')
    return ''
  let m2 = map(values(matches), '[0]+v:val')
  call sort(m2, 'lh#position#compare')
  let crt_pos = getpos('.')
  let dest = m2[0]+[0]
  " call assert_true(lh#position#is_before(crt_pos, dest))
  " Insert mode, :map-<expr>
  if mode == 'i' && crt_pos[1] == dest[1] " same-line
    " Need to know how many characters does this really represent, not the
    " number of bytes!
    let text = lh#position#extract(crt_pos, dest)
    let offset = lh#encoding#strlen(text)+1
    " produces a redo-able move sequence
    return lh#map#_move_cursor_on_the_current_line(offset)
    call setpos('.', dest)
    return mode == 'i' ? "\<Right>" : "a"

inoremap <silent> <Plug>MarkersJumpOutside <C-R>=lh#brackets#jump_outside({'mode': 'i'})<cr>
nnoremap <silent> <Plug>MarkersJumpOutside @=lh#brackets#jump_outside({'mode': 'n'})<cr>
xnoremap <silent> <Plug>MarkersJumpOutside <C-\><C-N>@=lh#brackets#jump_outside({'mode': 'x'})<cr>
    smap <silent> <Plug>MarkersJumpOutside <C-\><C-N>a<Plug>MarkersJumpOutside
  • Wow, that's nice. Will this all be available in plugin? Because as I can see now on github it isn't. – coredump Nov 9 '16 at 9:09
  • ^^' Yesterday I forgot to push this patch. The default keybinding for this feature is <C-PageDown>. See :h <Plug>MarkersJumpOutside. Note: I haven't intensively tested it yet. – Luc Hermitte Nov 9 '16 at 10:19

I) Don't create an insert mode mapping:

Insert mode is made to insert characters, when you need to move your cursor you should go back to normal mode or use ctrl+o to go to normal mode for only one command. (:h i_CTRL-O)

II) Use the built-in motion command:

In normal mode f+character goes to the next character (:h f). For example:

"foo bar| baz"

In normal mode pressing f" will put the cursor on the second " and a will start insert mode after this character.

Thus in the previous example from insert mode I would use esc+f+"+a. This has the benefit to seperate your edit in different undoable chunck of edits which is how you're supposed to use Vim.

Now if you still want to what you asked I think you'll have to create a function which will try to detect in which text object the cursor is currently and then move to the end of this text object. In my opinion that is more trouble than simply using Vim as it is meant to be used.


You may be interested by the % command which cycles through matching groups (:h matchit-%):

For example:

inoremap <leader>, <esc>%%a

Would do the trick for the matching brackets ((, {, () but not for the quotes.

  • I wanted something more complicated, I want one bindng for "go past one most enclosed delimiter. I.e. for "foo {a |b c} bar" -> "foo {a b c}| bar", (arg, "fo|o") -> (arg, "foo"|). – coredump Nov 7 '16 at 12:32
  • @coredump I understand, I'm just saying that a mapping doing that is probably not the best thing to do. See my last addition to the question inoremap <leader>, <Esc>%%a should be what you want for brackets. Note that Vim doesn't have a way to match quotes so doing the same with quotes would be pretty hard IMO. – statox Nov 7 '16 at 12:35
  • Your way will partly work, thanks, but it won't work for case like ("foo| {}", a). <esc>%%a will give ("foo {|}", a), when expected is ("foo {}", a)|. – coredump Nov 7 '16 at 12:39
  • 1
    @coredump Hence the beginning of my answer: use normal mode motions ;-) Now if you really want a mapping doing that you can try to create a vimscript function which will do what you want and then post it here as an answer: You'll learn a lot about Vim and vimscript doing so but I don't think it is worth the time. – statox Nov 7 '16 at 12:42

This is a simple one, you can add more delimiters in the [ ] group, but it won't check for the "inside" of anything:

inoremap <C-x><C-x> <Esc>/[\])}"]/e<CR>a,

It just searches for the 1st delimiter in the list, goes after it and appends a comma. So it roughly goes out of the "most inner" delimited group. If I'd use such a map, I'd skip the comma addition and add some more delimiters, to keep it more generic, and repeatable - say I hit <C-x><C-x> and I don't like where I landed, I just repeat the combo and go "out" after the next delimiter.

  • 1
    Looks good, i think i'll wrap it in function, saving previous search result and restoring it back after movment. Of course keeping it moregeneric is a good idea. – coredump Nov 7 '16 at 12:58
  • Also, you can create a similar move for the "backwards" direction. And maybe make both proper Vim motions (so you can use them in other modes, with count as number as delimiter skips), and build the Insert maps on those motions. :D – VanLaser Nov 7 '16 at 13:07
  • Actually i found out, maybe it's better to use searchpair() vim function, if it's possible to make it work on more than one delimiter at a time. – coredump Nov 7 '16 at 13:27
  • You can also inspire from github.com/terryma/vim-expand-region – VanLaser Nov 7 '16 at 13:39
  • Thanks, already wrote a function for this, i don't like it much, but it does the work. – coredump Nov 7 '16 at 18:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.