10

Currently my Vim highlights matching parentheses, brackets, quotes, etc. with Cyan background and white foreground -- the cursor can be moved between these with %. Thanks to my matchit.vim, I can also switch with % between if/end, for/end, etc. -- however these are not highlighted on selection.

How can I automatically highlight these matching pairs upon selection, like is automatically done with parentheses?

Furthermore, how can I modify the background color used for these pairs using :highlight?

Thanks in advance.


I've updated the answer by @Tommy A below to account for poorly specified matchit.vim groups, and other situations where the % operator does not return the cursor to the original position, ever. Check out the differences in the "while" loop. Anyone reading this thread is advised to use this version, to avoid infinite loops:

function! s:get_match_lines(line) abort
  " Loop until `%` returns the original line number; abort if
  " (1) the % operator keeps us on the same line, or
  " (2) the % operator doesn't return us to the same line after some nubmer of jumps
  let a:tolerance=25
  let a:badbreak=1
  let a:linebefore=-1
  let lines = []
  while a:tolerance && a:linebefore != line('.')
    let a:linebefore=line('.')
    let a:tolerance-=1
    normal %
    if line('.') == a:line
      " Note that the current line number is never added to the `lines`
      " list. a:line is the input argument 'line'; a is the FUNCTION BUFFER
      let a:badbreak=0
      break
    endif
    call add(lines, line('.'))
  endwhile
  "Return to original line no matter what, return list of lines to highlight
  execute "normal ".a:line."gg"
  if a:badbreak==1
    return []
  else
    return lines
  endif
endfunction

function! s:hl_matching_lines() abort
  " `b:hl_last_line` prevents running the script again while the cursor is
  " moved on the same line.  Otherwise, the cursor won't move if the current
  " line has matching pairs of something.
  if exists('b:hl_last_line') && b:hl_last_line == line('.')
    return
  endif
  let b:hl_last_line = line('.')
  " Save the window's state.
  let view = winsaveview()
  " Delete a previous match highlight.  `12345` is used for the match ID.
  " It can be anything as long as it's unique.
  silent! call matchdelete(12345)
  " Try to get matching lines from the current cursor position.
  let lines = s:get_match_lines(view.lnum)
  if empty(lines)
    " It's possible that the line has another matching line, but can't be
    " matched at the current column.  Move the cursor to column 1 to try
    " one more time.
    call cursor(view.lnum, 1)
    let lines = s:get_match_lines(view.lnum)
  endif
  if len(lines)
    " Since the current line is not in the `lines` list, only the other
    " lines are highlighted.  If you want to highlight the current line as
    " well:
    " call add(lines, view.lnum)
    if exists('*matchaddpos')
      " If matchaddpos() is availble, use it to highlight the lines since it's
      " faster than using a pattern in matchadd().
      call matchaddpos('MatchLine', lines, 0, 12345)
    else
      " Highlight the matching lines using the \%l atom.  The `MatchLine`
      " highlight group is used.
      call matchadd('MatchLine', join(map(lines, '''\%''.v:val.''l'''), '\|'), 0, 12345)
    endif
  endif
  " Restore the window's state.
  call winrestview(view)
endfunction
function! s:hl_matching_lines_clear() abort
  silent! call matchdelete(12345)
  unlet! b:hl_last_line
endfunction

" The highlight group that's used for highlighting matched lines.  By
" default, it will be the same as the `MatchParen` group.
highlight default link MatchLine MatchParen
augroup matching_lines
  autocmd!
  " Highlight lines as the cursor moves.
  autocmd CursorMoved * call s:hl_matching_lines()
  " Remove the highlight while in insert mode.
  autocmd InsertEnter * call s:hl_matching_lines_clear()
  " Remove the highlight after TextChanged.
  autocmd TextChanged,TextChangedI * call s:hl_matching_lines_clear()
augroup END
  • 2
    I know this is an old question, but I just saw it pop up onto the front page a moment ago. Just want to mention my new plugin match-up is designed to do exactly this, in a more robust way: github.com/andymass/vim-matchup (along with many other improvements over matchit). – Mass Dec 4 '17 at 19:07
  • Looks really useful, thanks for making this! I'll try it out. – Luke Davis Dec 4 '17 at 19:15
11

I thought this idea was interesting, so I gave it a shot. It will be particularly useful in dense files, such as HTML.

match lines

The following script simply lets matchit.vim do what it does while recording the line numbers. Explanations are in the script's comments.

matchlines.vim

function! s:get_match_lines(line) abort
  let lines = []

  " Loop until `%` returns the original line number
  while 1
    normal %
    if line('.') == a:line
      " Note that the current line number is never added to the `lines`
      " list.
      break
    endif
    call add(lines, line('.'))
  endwhile

  return lines
endfunction

function! s:hl_matching_lines() abort
  " `b:hl_last_line` prevents running the script again while the cursor is
  " moved on the same line.  Otherwise, the cursor won't move if the current
  " line has matching pairs of something.
  if exists('b:hl_last_line') && b:hl_last_line == line('.')
    return
  endif

  let b:hl_last_line = line('.')

  " Save the window's state.
  let view = winsaveview()

  " Delete a previous match highlight.  `12345` is used for the match ID.
  " It can be anything as long as it's unique.
  silent! call matchdelete(12345)

  " Try to get matching lines from the current cursor position.
  let lines = s:get_match_lines(view.lnum)

  if empty(lines)
    " It's possible that the line has another matching line, but can't be
    " matched at the current column.  Move the cursor to column 1 to try
    " one more time.
    call cursor(view.lnum, 1)
    let lines = s:get_match_lines(view.lnum)
  endif

  if len(lines)
    " Since the current line is not in the `lines` list, only the other
    " lines are highlighted.  If you want to highlight the current line as
    " well:
    " call add(lines, view.lnum)
    if exists('*matchaddpos')
      " If matchaddpos() is availble, use it to highlight the lines since it's
      " faster than using a pattern in matchadd().
      call matchaddpos('MatchLine', lines, 0, 12345)
    else
      " Highlight the matching lines using the \%l atom.  The `MatchLine`
      " highlight group is used.
      call matchadd('MatchLine', join(map(lines, '''\%''.v:val.''l'''), '\|'), 0, 12345)
    endif
  endif

  " Restore the window's state.
  call winrestview(view)
endfunction

function! s:hl_matching_lines_clear() abort
  silent! call matchdelete(12345)
  unlet! b:hl_last_line
endfunction


" The highlight group that's used for highlighting matched lines.  By
" default, it will be the same as the `MatchParen` group.
highlight default link MatchLine MatchParen

augroup matching_lines
  autocmd!
  " Highlight lines as the cursor moves.
  autocmd CursorMoved * call s:hl_matching_lines()
  " Remove the highlight while in insert mode.
  autocmd InsertEnter * call s:hl_matching_lines_clear()
  " Remove the highlight after TextChanged.
  autocmd TextChanged,TextChangedI * call s:hl_matching_lines_clear()
augroup END

I don't really like this happening on CursorMoved, though. I think it's better as a key map that can be used when I need it:

nnoremap <silent> <leader>l :<c-u>call <sid>hl_matching_lines()<cr>
  • You could use the matchaddpos function instead. It is slightly faster and if you highlight the entire line anyway, it will simplify things a little bit. – Karl Yngve Lervåg Jul 10 '16 at 16:01
  • 1
    @KarlYngveLervåg Good point. I subconsciously avoid it because it's still a relatively new function (v7.4.330 I think) and it bit me in the ass once. I'll update the answer to use it. – Tommy A Jul 10 '16 at 16:05
  • This is absolutely perfect, thanks so much! Good Vimscript practice too; will try to understand each line. I imagine this could be quite popular if you're the first to write this sort of utility. – Luke Davis Jul 11 '16 at 22:14
  • @LukeDavis There is an undesired effect that comes from this that I noticed: it will screw up the jump list. I came up with a way to sort of fix it by using <c-o> for the number of times that a match was found and it works in a way. The problem is there's a bug in matchit.vim that adds the top line of the window to the jump list. It has been acknowledged, but there doesn't seem to be any rush to fix it. – Tommy A Jul 11 '16 at 22:21
  • @TommyA Hey, thanks again for this utility. I actually find on my computer the delay with the CursorMove autocmd is pretty negligible. I updated your function s:get_match_lines(line) to help guard against infinite loops, which was becoming a big problem for me in certain weird contexts. Unfortunately matchit.vim is full of flaws. See my edit above and let me know if you have any suggestions; I'm a vimscript beginner. – Luke Davis Feb 3 '17 at 22:05

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.