Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

https://github.com/redguardtoo/evil-matchit

Vim matchit ported into Emacs
https://github.com/redguardtoo/evil-matchit

Last synced: 8 days ago
JSON representation

Vim matchit ported into Emacs

Lists

README

        

* evil-matchit
[[https://github.com/redguardtoo/evil-matchit/actions/workflows/test.yml][https://github.com/redguardtoo/evil-matchit/actions/workflows/test.yml/badge.svg]]
[[https://elpa.nongnu.org/nongnu/evil-matchit.html][file:https://elpa.nongnu.org/nongnu/evil-matchit.svg]]
[[http://melpa.org/#/evil-matchit][file:http://melpa.org/packages/evil-matchit-badge.svg]]
[[http://stable.melpa.org/#/evil-matchit][file:http://stable.melpa.org/packages/evil-matchit-badge.svg]]

Vim [[http://www.vim.org/scripts/script.php?script_id=39][matchit.vim]] by Benji Fisher is ported into Emacs.

Press "%" to jump between matched tags ("

" and "
" in html, etc).

Built-in supported languages and documents:
- HTML
- Python
- Java
- C++/C
- Javascript
- Typescript
- React JSX (rjsx-mode, react-mode)
- JSON
- OCaml
- Perl
- Yaml
- Latex
- MATLAB/Octave
- CMake
- Markdown
- Org (matching tags of other languages embedded in org file is also supported!)
- Ruby
- Elixir
- Bash
- Lua
- PHP
- Fortran
- SQL
- Laravel Blade Templating
- Vim script
- Verilog
- Diff/Patch
- Shell/Terminal bundled in Emacs
- Emacs email (message-mode)
- VCS (Git/Subversion/Perforce ...) merge conflicts

If [[https://www.emacswiki.org/emacs/Evil][EVIL]] is installed, this package use EVIL as its vi layer.

If EVIL is not installed, most commands still work. So *EVIL is only optional dependency*.

Tested on Emacs 25, 26, 27, 28

* Why use evil-matchit
- Support any modern languages (html/java/c/c++/python/latex/javascript ...)
- Powerful. If you mix jsp, freemarker, html, jquery template or any weird syntax into one file, it still works!
- Extendable. Write a plugin for it takes only 5 minutes

Screen cast for python:
[[file:screencast.gif]]

* Install
It's already uploaded to [[http://melpa.org/]].

* Set up
** EVIL is used
Insert below code into =~/.emacs= to setup key bindings:
#+BEGIN_SRC lisp
(global-evil-matchit-mode 1)
#+END_SRC

Alternatively, you can enable =evil-matchit-mode= along a major mode by adding =turn-on-evil-matchit-mode= to the mode hook.
** EVIL is not used
No setup is required.

* Usage
** EVIL is used
You can press "%" or =M-x evilmi-jump-items= to jump between tag pair in normal mode or visual mode (you press "v" to switch to visual mode).

Please note evil-matchit is smart enough to *detect the tag automatically*.

Tag pair could be open/closed html tag, or character pair like "{}" "[]" "()", or the single/double quote(s) at the two ends of the string.

Inner/outer text object "%" is also created. It roughly equals the region when you press "%" from evil-matchit.

Press "va%" to select line(s) wrapped by tags including tags themselves. =M-x evilmi-select-items= does the same thing.

Press "da%" to delete line(s) wrapped by tags including tags themselves. =M-x evilmi-delete-items= does the same thing.

All commands support numeric argument like "3%", "5va%" or "9da%"

Pressing "3%" jumps to a line 3 percentage down the file. It's the default behavior in original =evil-mode=. You can =(setq evilmi-may-jump-by-percentage nil)= to turn off this feature. Then "3%" will jump 3 times.

If you need visually select lines, you could use =evilmi-select-items=.

This is actually an advantage of Emacs, you can tweak the select region without go into visual state at all.

** EVIL is not used
Use =evilmi-jump-items-native= to replace =evilmi-jump-items=. Evil text object "%" is de-activated.

But all the other commands like =evilmi-delete-items= and =evilmi-select-items= still work.

* Tips
** Toggle other modes before&after jumping to the matched tag
It's reported [[https://github.com/redguardtoo/evil-matchit/issues/138][some mode is not compatible with this package]].

You can use =evilmi-jump-hook= to turn off the mode before jumping to the matched tag.

Then turn on it after the jump using the same hook.

Here is an example to toggle =global-tree-sitter-mode=,
#+begin_src elisp
(add-hook 'evilmi-jump-hook
(lambda (before-jump-p)
(global-tree-sitter-mode (not before-jump-p))))
#+end_src
** Support new major modes
In order to apply three matching rules =evilmi-template=, =evilmi-simple=, and =evilmi-html= on =mhtml-mode=, please insert below code *after* your evil-matchit setup:
#+begin_src elisp
(evilmi-load-plugin-rules '(mhtml-mode) '(template simple html))
#+end_src
** Use evilmi-select-items instead press "%" in evil-visual-state
=evilmi-select-items= is more robust and provides more functionality. It works even when =evil-mode= is not loaded.

So you'd better stick to =evilmi-select-item= if possible.
** Add new tags into existing languages
Use ruby as an example.

If you want to add more tags into ruby, you can do two things:
- You need define the regular expression to extract keyword
- You need define the open/middle/closed tags

Open evil-matchit-ruby.el whole structure is like,
#+begin_src elisp
(defvar evilmi-ruby-extract-keyword-howtos '())
(defvar evilmi-ruby-match-tags '())
;; more code here ...
(provide 'evil-matchit-ruby)
#+end_src

So you configuration in =~/.emacs= is as below:
#+begin_src elisp
(with-eval-after-load "evil-matchit-ruby"
(push '("^[ \t]*\\([a-z]+\\)\\( .*\\| *\\)$" 1) evilmi-ruby-extract-keyword-howtos)
(push '(("unless" "if") ("elsif" "else") "end")) evilmi-ruby-match-tags)
#+end_src

** Re-define keybinding
All you need to do is to define function =evilmi-customize-keybinding= before turning on =evil-matchit-mode=:

The shortcut =%= is defined in =evilmi-shortcut=. It's the name of text object and shortcut of =evilmi-jump-items=. Some people prefer set it
to "m".

Change keybinding of =evilmi-jump-items= *and* name of the text object,
#+begin_src elisp
(setq evilmi-shortcut "m")
(global-evil-matchit-mode 1)
#+end_src

Change keybinding *only*,
#+BEGIN_SRC elisp
(defun evilmi-customize-keybinding ()
(evil-define-key 'normal evil-matchit-mode-map
"%" 'evilmi-jump-items))
(global-evil-matchit-mode 1)
#+END_SRC
** Jump between the two end of the "string"
Please note the definition of "string" could be *customized* by user.

For example, we could treat C comment as string wrapper by "/".

Here is the setup to jump between the two ends of the C comment:
#+begin_src elisp
(setq evilmi-quote-chars (string-to-list "'\"/"))
#+end_src
** Match case-sensitive tags?
It's decided by the Emacs global variable "case-fold-search". You need not care about it because the major mode will set this flag automatically.
** Python
You can turn on =evilmi-always-simple-jump= to match brackets at first.

Thus, you disable our *advanced algorithm* which I highly recommend.

Some people may prefer simpler algorithm in =python-mode=.
* Developer guide
** Quick start to support new language
Simple. You only need define two functions and tell evil-matchit in which major mode they should be used.

A complete setup to insert into "~/.emacs":
#+BEGIN_SRC elisp
;; detect tag in current line and return the result in variable rlt
;; the rlt will be used by evilmi-mylang-jump as the first parameter.
;; if NO tag found, the rlt SHOULD be nil
;;
;; @return the data to be used by evilmi-mylang-jump which should be a list
;; the first element of the list is the position of cursor before jump
;; we use it to select/delete tag. The other elements of the list could
;; be any data type
(defun evilmi-mylang-get-tag ()
(list position-of-open-end "anything-you-like" "anything-you-like"))

;; @parama rlt result from evilmi-mylang-get-tag
;; @param NUM numeric argument when user press "%" to match tag
;; @return the matching tag position in theory, useful only for
;; selecting or deleting text between matching tags and tags
(defun evilmi-mylang-jump (info num)
(message "info=%s" info)
;; if we need select region between tags (including tags itself)
;; we get the beginning of region by reading the first element of
;; info
(push-mark (nth 0 info) t t)
;; say 999 is the where we jump to
(goto-char 999)
;; If you need know where is the end of the region for region operation,
;; you need return the end of region at the end of function
;; region operation means selection/deletion of region.
888)

;; Notify evil-matchit how to use above functions
(evilmi-load-plugin-rules '(mylang-mode) '(mylang))
#+END_SRC

Place above code into your =~/.emacs=, after the line "(global-evil-matchit-mode 1)"

** Use SDK
For example, it only takes 3 steps to create a new rule =script= to match tags in script like Ruby/Lua/Bash/VimScript,

Step 1, create =evil-matchit-script.el=,
#+BEGIN_SRC elisp
(require 'evil-matchit-sdk)

;; ruby/bash/lua/vimrc
(defvar evilmi-script-match-tags
'((("unless" "if") ("elif" "elsif" "elseif" "else") ( "end" "fi" "endif"))
("begin" ("rescue" "ensure") "end")
("case" ("when" "else") ("esac" "end"))
(("fun!" "function!" "class" "def" "while" "function" "do") () ("end" "endfun" "endfunction"))
("repeat" () "until"))
"The table we look up match tags. This is a three column table.
The first column contains the open tag(s).
The second column contains the middle tag(s).
The third column contains the closed tags(s).
The forth *optional* column defines the relationship between open and close tags. It could be MONOGAMY
")

;;;###autoload
(defun evilmi-script-get-tag ()
(evilmi-sdk-get-tag evilmi-script-match-tags
evilmi-sdk-extract-keyword-howtos))

;;;###autoload
(defun evilmi-script-jump (rlt num)
(evilmi-sdk-jump rlt
num
evilmi-script-match-tags
evilmi-sdk-extract-keyword-howtos))

(provide 'evil-matchit-script)
#+END_SRC

Step 2, make sure the directory of =evil-matchit-script.el= is added into =load-path=.

Step 3, add below code to =~/.emacs.=,
#+BEGIN_SRC lisp
(evilmi-load-plugin-rules '(ruby-mode lua-mode) '(script))
#+END_SRC
*** Support languages using indentation to identify a block of code
It's easy to support such language (Python, Yaml, ...).

Here is a minimum example to support yaml,
#+begin_src elisp
(require 'evil-matchit-indent)

(defun evilmi-yaml-get-tag ()
(evilmi-indent-get-tag))

(defun evilmi-yaml-jump (info num)
(let* ((evilmi-spaces-per-tab 2))
(evilmi-indent-jump info)))

(evilmi-load-plugin-rules '(yaml-mode) '(yaml))
#+end_src
** APIs
- evilmi-load-plugin-rules
* Contact me
Report bugs at [[https://github.com/redguardtoo/evil-matchit]].

* License
This program is free software: you can redistribute it and/or modify it under the terms of the [[file:LICENSE][GNU General Public License]] as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [[file:LICENSE][GNU General Public License]] for more details.