If your cursor is on top of a "{", pressing % will send it to its matching "}". I like that behaviour.

However, if your cursor is behind a "{", pressing % will "look ahead", and also send the cursor to its matching "}".

I'd prefer if vim only considered what was immediately under the cursor, and didn't try to look ahead, simply staying still if there's no match. Is that possible?

  • are you using matchit? what filetype is this? Commented Sep 10, 2021 at 7:49
  • @ChristianBrabandt No, I'm using matchup though. Why is the filetype relevant? The % operator works even on an empty file type. Commented Sep 10, 2021 at 7:58
  • I prefer current behavior and it is specified in the docs. There is no 'option' to change it other than implementing your own version of %
    – Maxim Kim
    Commented Sep 10, 2021 at 8:21

3 Answers 3


Since a comment under the question mentions that OP is using the plugin vim-matchup, I'll give the match-up specific solution. The plugin provides matchup#custom#define_motion to facilitate such customization. This motion boils down to checking if there is a delimiter at the cursor and fetching the count-th next match. It will work in normal, visual, and operator pending mode, and g% (the opposite of %) is also provided.

function! s:percent(info, opts) abort
  let l:delim = matchup#delim#get_current('all', 'both_all')
  if !empty(l:delim)
    let l:matches = matchup#delim#get_matching(l:delim, 1)
    if len(l:matches)
      for _ in range(a:info.count1)
        let l:delim = l:delim.links[a:opts.down ? 'next': 'prev']
      return matchup#custom#suggest_pos(l:delim, a:opts)

call matchup#custom#define_motion('nox', '%', funcref('s:percent'), { 'down': 1 })
call matchup#custom#define_motion('nox', 'g%', funcref('s:percent'), { 'down': 0 })

As a bonus, here is a second, perhaps more straightforward method. Here we just check if there is a delimiter under the cursor, and if so use match-up's % map. Otherwise, do nothing. Unlike the first solution, this doesn't work for modes other than normal.

function! s:test_cur()
  call matchup#perf#timeout_start(1000)
  return !empty(matchup#delim#get_current('all', 'both_all'))

nmap <expr> % <sid>test_cur() ? '<plug>(matchup-%)' : '<Ignore>'
  • For anyone else having problems running the solution: Remember that plugins run after init.vim, so if you put it there matchup will overwrite the command. Make sure that these are executed after the matchup plugin (e.g. in the after/plugin/matchup.vim) Commented Oct 11, 2021 at 12:30

The only way I can see is to create your own mapping that should do what you want, for example:

nnoremap <expr> % match(getline('.')[col('.')-1], '[()\[\]{}]') == -1 ? "" : "%"

The issue you might face though is that it wouldn't work with "extended brackets" like html tags and some others.


A possible version that hasn't been tested:

const s:old_percent = maparg('%')

function s:doPercent() abort
  if empty(s:old_percent)
    normal! %
    execute s:old_percent

function MyPercent() abort
  const start = getcurpos()
  call s:doPercent()
  call s:doPercent()
  if start[1] isnot# getcurpos()[1] ||start[2] isnot# getcurpos()[2]
    call set_cursor('.', start)
    call s:doPercent()

noremap % <cmd>call MyPercent()<CR>

As Rich points out

In case any readers that don't already know Vimscript are curious as to how this differs from the existing answer, this one works by doing the % operation twice and seeing if the cursor ends up where it started, followed by either resetting the cursor to the original position or doing a third % depending on the results.

I'm relying on the fact that (at least with brackets!) %% will stay in the same place if you are on one of the brackets, or jump forward to said bracket if you are not on it. This is not going to work with constructs like if/else/endif matching, because we can't know how many times to run %.

The rest (s:doPercent and s:old_percent) are some boilerplate trying to work better with plugins, but frankly probably still won't always work (see the last sentence of the previous paragraph).

It should work for very vanilla % though (not even the built-in matchit, since it supports %-cycles between groups longer than 2 elements).

  • Thanks @Rich, I'm going to edit that in with attribution. Wish I could give you any associated rep :(
    – D. Ben Knoble
    Commented Oct 5, 2021 at 15:54
  • @Rich I wouldn't call it "reimplements from scratch" it uses the same %, only triggers it for certain chars. :)
    – Maxim Kim
    Commented Oct 5, 2021 at 17:31
  • 1
    @MaximKim Whoops! I didn't actually read your code properly!! Sorry for misrepresenting your answer.
    – Rich
    Commented Oct 6, 2021 at 7:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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