andymass / vim-matchup

vim match-up: even better % :facepunch: navigate and highlight matching words :facepunch: modern matchit and matchparen. Supports both vim and neovim + tree-sitter.
https://www.vim.org/scripts/script.php?script_id=5624
MIT License
1.71k stars 73 forks source link
highlighting-matches matching-pairs matchparen motions nvim-treesitter parenthesis-matching vim vim-plugin vimscript-5624

and in this corner...

vim match-up :fist_right::fist_left:

match-up is a plugin that lets you highlight, navigate, and operate on sets of matching text. It extends vim's % key to language-specific words instead of just single characters.

Screenshot

Table of contents

Overview

match-up can be used as a drop-in replacement for the classic plugin matchit.vim. match-up aims to enhance all of matchit's features, fix a number of its deficiencies and bugs, and add a few totally new features. It also replaces the standard plugin matchparen, allowing all of matchit's words to be highlighted along with the matchpairs ((){}[]).

See detailed feature documentation for more information. This plugin:

Installation

If you use vim-plug, then add the following line to your vimrc's plugin section:

Plug 'andymass/vim-matchup'

and then use :PlugInstall.

Or, if you use packer, add it to your init.vim

return require('packer').startup(function(use)
  use {
    'andymass/vim-matchup',
    setup = function()
      -- may set any options here
      vim.g.matchup_matchparen_offscreen = { method = "popup" }
    end
  }
end)

and run :PackerSync or similar.

See Tree-sitter integration for information on how to enable tree-sitter matching with neovim.

Note: I do not recommend using alternative loading strategies such as event = 'VimEnter' or event = 'CursorMoved' as match-up already loads a minimal amount of code on start-up. It may work, but if you run into issues, remove the event key as a first debugging step.

With LunarVim, tree-sitter integration can be enabled as follows:

{
  "andymass/vim-matchup",
  setup = function()
    vim.g.matchup_matchparen_offscreen = { method = "popup" }
  end,
},

lvim.builtin.treesitter.matchup.enable = true

You can use any other plugin manager such as vundle, dein, neobundle, or pathogen,

match-up should automatically disable matchit and matchparen, but if you are still having trouble, try placing this near the top of your vimrc:

let g:loaded_matchit = 1

See Interoperability for more information about working together with other plugins.

Tree-sitter integration

Note: Currently this feature is possible in neovim only. Only the latest version of neovim is supported.

match-up has support for language syntax provided by tree-sitter. The list of supported languages is available here.

This feature requires manual opt-in in your init.vim and requires nvim-treesitter to be installed.

Plug 'nvim-treesitter/nvim-treesitter'
lua <<EOF
require'nvim-treesitter.configs'.setup {
  matchup = {
    enable = true,              -- mandatory, false will disable the whole extension
    disable = { "c", "ruby" },  -- optional, list of language that will be disabled
    -- [options]
  },
}
EOF

Beside enable and disable, the following options are available, all defaulting to disabled:

Screenshot:

Features

feature match-up matchit matchparen
(a.1) jump between matching words :thumbsup: :thumbsup: :x:
(a.2) jump to open & close words :thumbsup: :thumbsup: :x:
(a.3) jump inside (z%) :thumbsup: :x: :x:
(b.1) full set of text objects :thumbsup: :question: :x:
(b.2) delete surrounding matched words :thumbsup: :x: :x:
(c.1) highlight (), [], & {} :thumbsup: :x: :thumbsup:
(c.2) highlight all matching words :thumbsup: :x: :x:
(c.3) display matches off-screen :thumbsup: :x: :x:
(c.4) show where you are (breadcrumbs) :thumbsup: :x: :x:
(d.1) (neovim) tree-sitter integration :thumbsup: :x: :x:

Legend: :thumbsup: supported. :question: poorly implemented, broken, or uncertain. :x: not possible.

Detailed feature documentation

What do we mean by open, close, mid? This depends on the specific file type and is configured through the variable b:match_words. Here are a couple examples:

vim-script

if l:x == 1
  call one()
elseif l:x == 2
  call two()
else
  call three()
endif

For the vim-script language, match-up understands the words if, else, elseif, endif and that they form a sequential construct. The "open" word is if, the "close" word is endif, and the "mid" words are else and elseif. The if/endif pair is called an "open-to-close" block and the if/else, else/elsif, and elseif/endif are called "any" blocks.

C, C++

#if 0
#else
#endif

void some_func() {
    if (true) {
      one();
    } else if (false && false) {
      two();
    } else {
      three();
    }
}

Since in C and C++, blocks are delimited using braces ({ & }), match-up will recognize { as the open word and } as the close word. It will ignore the if and else if because they are not defined in vim's default C file type plugin. (Note: In neovim, this is optionally supported via Tree-sitter)

On the other hand, match-up will recognize the #if, #else, #endif preprocessor directives.

(a.1) jump between matching words

(a.2) jump to open and close words

(a.3) jump inside

  █ call somefunction(param1, param2)

dz% produces

  param1, param2)

but in

  █ call somefunction(      param1, param2)

dz% also produces

  param1, param2)

(b.1) full set of text objects

See here for some examples and important special cases.

(c.1) highlight (), [], and {}

match-up emulates vim's matchparen to highlight the symbols contained in the matchpairs setting.

(c.2) highlight all matches

To disable match highlighting at startup, use let g:matchup_matchparen_enabled = 0 in your vimrc. See here for more information and related options.

You can enable highlighting on the fly using :DoMatchParen. Likewise, you can disable highlighting at any time using :NoMatchParen.

After start-up, is better to use :NoMatchParen and :DoMatchParen to toggle highlighting globally than setting the global variable since these commands make sure not to leave stale matches around.

(c.3) display matches off screen

If a open or close which would have been highlighted is on a line positioned outside the current window, the match is shown in the status line or popup window. If both the open and close match are off-screen, the close match is preferred. See the option g:matchup_matchparen_offscreen for more details.

For popup style (supported in recent vim and neovim versions):

let g:matchup_matchparen_offscreen = {'method': 'popup'}

For status line style (default):

let g:matchup_matchparen_offscreen = {'method': 'status'}

(c.4) where am I?

If you are lost, you can ask match-up where you are using

:MatchupWhereAmI?

This echos your position in the code in a breadcrumb-style by finding successive matching words, like doing [% repeatedly.

It's useful to bind this to a key (not bound by default)

nnoremap <c-k> :<c-u>MatchupWhereAmI?<cr>

If you are really lost, you can ask a bit harder to get a more detailed print out.

:MatchupWhereAmI??

Inclusive and exclusive motions

In vim, character motions following operators (such as d for delete and c for change) are either inclusive or exclusive. This means they either include the ending position or not. Here, "ending position" means the line and column closest to the end of the buffer of the region swept over by the motion. match-up is designed so that d]% inside a set of parenthesis behaves exactly like d]), except generalized to words.

Put differently, forward exclusive motions will not include the close word. In this example, where is the cursor position,

if █x | continue | endif

pressing d]% will produce (cursor on the e)

if endif

To include the close word, use either dv]% or v]%d. This is also compatible with vim's d]) and d]}.

Operators over backward exclusive motions will instead exclude the position the cursor was on before the operator was invoked. For example, in

  if █x | continue | endif

pressing d[% will produce

  █x | continue | endif

This is compatible with vim's d[( and d[{.

Unlike ]%, % is an inclusive motion. As a special case for the d (delete) operator, if d% leaves behind lines white-space, they will be deleted also. In effect, it will be operating line-wise. As an example, pressing d% will leave behind nothing.

   █(

   )

To operate character-wise in this situation, use dv% or v%d. This is vim compatible with the built-in d% on matchpairs.

Line-wise operator/text-object combinations

Normally, the text objects i% and a% work character-wise. However, there are some special cases. For certain operators combined with i%, under certain conditions, match-up will effectively operate line-wise instead. For example, in

if condition
 █call one()
  call two()
endif

pressing di% will produce

if condition
endif

even though deleting condition would be suggested by the object i%. The intention is to make operators more useful in some cases. The following rules apply:

  1. The operator must be listed in g:matchup_text_obj_linewise_operators. By default this is d and y (e.g., di% and ya%).
  2. The outer block must span multiple lines.
  3. The open and close delimiters must be more than one character long. In particular, di% involving a (...) block will not be subject to these special rules.

To prevent this behavior for a particular operation, use vi%d. Note that special cases involving indentation still apply (like with |i)| etc).

To disable this entirely, remove the operator from the following variable,

let g:matchup_text_obj_linewise_operators = [ 'y' ]

Note: unlike vim's built-in i), ab, etc., i% does not make an existing visual mode character-wise.

A second special case involves da%. In this example,

    if condition
     █call one()
      call two()
    endif

pressing da% will delete all four lines and leave no white-space. This is vim compatible with da(, dab, etc.

Options

To disable the plugin entirely,

let g:matchup_enabled = 0

default: 1

To disable a particular module,

let g:matchup_matchparen_enabled = 0
let g:matchup_motion_enabled = 0
let g:matchup_text_obj_enabled = 0

defaults: 1

To enable the delete surrounding (ds%) and change surrounding (cs%) maps,

let g:matchup_surround_enabled = 1

default: 0

To enable the experimental transmute module,

let g:matchup_transmute_enabled = 1

default: 0

To configure the number of lines to search in either direction while using motions and text objects. Does not apply to match highlighting (see g:matchup_matchparen_stopline instead).

let g:matchup_delim_stopline = 1500

default: 1500

To disable matching within strings and comments,

let g:matchup_delim_noskips = 1   " recognize symbols within comments
let g:matchup_delim_noskips = 2   " don't recognize anything in comments

default: 0 (matching is enabled within strings and comments)

Variables

match-up understands the following variables from matchit.

These are set in the respective ftplugin files. They may not exist for every file type. To support a new file type, create a file after/ftplugin/{filetype}.vim which sets them appropriately.

Module matchparen

To disable match highlighting at startup, use let g:matchup_matchparen_enabled = 0 in your vimrc. Note: vim's built-in plugin |pi_paren| plugin is also disabled. The variable g:loaded_matchparen has no effect on match-up.

Customizing the highlighting colors

match-up uses the MatchParen highlighting group by default, which can be configured. For example,

:hi MatchParen ctermbg=blue guibg=lightblue cterm=italic gui=italic

You may want to put this inside a ColorScheme autocmd so it is preserved after colorscheme changes:

augroup matchup_matchparen_highlight
  autocmd!
  autocmd ColorScheme * hi MatchParen guifg=red
augroup END

You can also highlight words differently than parentheses using the MatchWord highlighting group. You might do this if you find the MatchParen style distracting for large blocks.

:hi MatchWord ctermfg=red guifg=blue cterm=underline gui=underline

There are also MatchParenCur and MatchWordCur which allow you to configure the highlight separately for the match under the cursor.

:hi MatchParenCur cterm=underline gui=underline
:hi MatchWordCur cterm=underline gui=underline

The matchparen module can be disabled on a per-buffer basis (there is no command for this). By default, when disabling highlighting for a particular buffer, the standard plugin matchparen will still be used for that buffer.

let b:matchup_matchparen_enabled = 0

default: 1

If this module is disabled on a particular buffer, match-up will still fall-back to the vim standard plugin matchparen, which will highlight matchpairs such as (), [], & {}. To disable this,

let b:matchup_matchparen_fallback = 0

default: 1

A common usage of these options is to automatically disable matchparen for particular file types;

augroup matchup_matchparen_disable_ft
  autocmd!
  autocmd FileType tex let [b:matchup_matchparen_fallback,
      \ b:matchup_matchparen_enabled] = [0, 0]
augroup END

Whether to highlight known words even if there is no match:

let g:matchup_matchparen_singleton = 1

default: 0

Dictionary controlling the behavior with off-screen matches.

let g:matchup_matchparen_offscreen = { ... }

default: {'method': 'status'}

If empty, this feature is disabled. Else, it should contain the following optional keys:

The number of lines to search in either direction while highlighting matches. Set this conservatively since high values may cause performance issues.

let g:matchup_matchparen_stopline = 400  " for match highlighting only

default: 400

highlighting timeouts

Adjust timeouts in milliseconds for matchparen highlighting:

let g:matchup_matchparen_timeout = 300
let g:matchup_matchparen_insert_timeout = 60

default: 300, 60

deferred highlighting

Deferred highlighting improves cursor movement performance (for example, when using hjkl) by delaying highlighting for a short time and waiting to see if the cursor continues moving;

let g:matchup_matchparen_deferred = 1

default: 0 (disabled)

Note: this feature is only available if your vim version has timers and the function timer_pause, version 7.4.2180 and after.

Adjust delays in milliseconds for deferred highlighting:

let g:matchup_matchparen_deferred_show_delay = 50
let g:matchup_matchparen_deferred_hide_delay = 700

default: 50, 700

Note: these delays cannot be changed dynamically and should be configured before the plugin loads (e.g., in your vimrc).

highlight surrounding

To highlight the surrounding delimiters until the cursor moves, use a map such as the following

nmap <silent> <F7> <plug>(matchup-hi-surround)

There is no default map for this feature.

You can also highlight surrounding delimiters always as the cursor moves.

let g:matchup_matchparen_deferred = 1
let g:matchup_matchparen_hi_surround_always = 1

default: 0 (off)

This can be set on a per-buffer basis:

autocmd FileType tex let b:matchup_matchparen_hi_surround_always = 1

Note: this feature requires deferred highlighting to be supported and enabled.

Module motion

In vim, {count}% goes to the {count} percentage in the file. match-up overrides this motion for small {count} (by default, anything less than 7). To allow {count}% for {count} less than 12,

g:matchup_motion_override_Npercent = 11

To disable this feature, and restore vim's default {count}%,

g:matchup_motion_override_Npercent = 0

To always enable this feature, use any value greater than 99,

g:matchup_motion_override_Npercent = 100

default: 6

If enabled, cursor will land on the end of mid and close words while moving downwards (%/]%). While moving upwards (g%, [%) the cursor will land on the beginning. To disable,

let g:matchup_motion_cursor_end = 0

default: 1

Module text_obj

Modify the set of operators which may operate line-wise

let g:matchup_text_obj_linewise_operators = ['d', 'y']

default: ['d', 'y']

FAQ

Interoperability

vimtex, for LaTeX documents

By default, match-up will be disabled automatically for tex files when vimtex is detected. To enable match-up for tex files, use

let g:matchup_override_vimtex = 1

match-up's matching engine is more advanced than vimtex's and supports middle delimiters such as \middle| and \else. The exact set of delimiters recognized may differ between the two plugins. For example, the mappings da% and dad will not always match, particularly if you have customized vimtex's delimiters.

Surroundings

match-up provides built-in support for vim-surround-style ds% and cs% operations (let g:matchup_surround_enabled = 1). If vim-surround is installed, you can use vim-surround replacements such as cs%). % cannot be used as a replacement. An alternative plugin is vim-sandwich, which allows more complex surround replacement rules but is not currently supported.

Auto-closing plugins

match-up does not provide auto-complete or auto-insertion of matches. See for instance one of the following plugins for this;

Matchit

match-up tries to work around matchit.vim in all cases, but if you experience problems, read the following:

Matchparen emulation

match-up loads matchparen if it is not already loaded. Ordinarily, match-up disables matchparen's highlighting and emulates it to highlight the symbol contained in the 'matchpairs' option (by default (), [], and {}). If match-up is disabled per-buffer using b:matchup_matchparen_enabled, match-up will use matchparen instead of its own highlighting. See b:matchup_matchparen_fallback for more information.

Acknowledgments

Origins

match-up was originally based on @lervag's vimtex. The concept and style of this plugin and its development are heavily influenced by vimtex. :beers:

Other inspirations

Development

Reporting problems

Thorough issue reports are encouraged. Please read the issue template first. Be as precise and detailed as possible when submitting issues.

Feature requests are also welcome.

Contributing

Please read the contribution guidelines before contributing.

Contributions are welcome!