Match parens with Emacs builtins

posted on 2023-06-02

When I was using ViM one of the most used movement keys was the jump-to-matching-parens (%) one. I really liked that it was symmetrical – I could jump to a matching parenthesis and pressing the key once more it jumped right back were I was. This was one of the movement keys I felt the lack of when I stopped using Emacs evil-mode. Emacs do have a really useful forward-sexp and backward-sexp functions that can be used to get similar functionality but they are not symmetrical. One can jump forward-sexp and executing backward-sexp will not bring you to the previous position – it all depends on the context of the s-expression you have jumped to.

I have used external packages to provide similar functionality, the last one was fingertip – lightweight package that uses tree-sitter and one of its many functions was match-parens. It had some kind of fallback that worked well with modes that do not use tree-sitter (like emacs-lisp-mode), but it stopped working in non-tree-sitter modes after one of the updates. Instead of trying to bring this fallback mechanism back (not even sure if it was not an error and not intended behaviour) I quickly rolled my own function using the Emacs builtins and made it behave like I remember the ViM’s % key.

(defun cc/jump-to-matching-paren ()
  "Jumps to a matching parenthesis using forward/backward-sexp functions."
  (interactive)
  (let* ((cur-char (char-after))
         (parens-begin '(?\( ?\[ ?\{ ?\<))
         (parens-end   '(?\) ?\] ?\} ?\>)))
    (cond
     ;; current character ends parenthesis -- we jump backward.
     ((memq cur-char parens-begin)
      ;; even if forward-sexp would position cursor after the paren
      ;; we don't need to backward-char as this function and Emacs
      ;; highlighting do treat such a parenthesis-before-cursor
      ;; as a candidate to match with previous one -- i.e. it works
      ;; as intended even though it is asymmetrical
      (forward-sexp))
     ;; current character begin parenthesis -- we jump forward.
     ;; OR: previous character ends parenthesis and current is not parenthesis,
     ;; this may happen at the end of line when we want to jump to matching last
     ;; parenthesis when there are few parenthesis near themselves.
     ((or (memq cur-char parens-end) (memq (char-before) parens-end))
      (backward-sexp)
      ;; when only one sexp is on the line, backward-sexp will position cursor
      ;; after the paren (i.e. inside the paren); we make sure we go back to the actual
      ;; paren when this happens.
      (when (not (memq (char-after) parens-begin))
        (backward-char))
      )
     (t (message "Current character doesn't match known parenthesis „([{<>}])”.")))))

The function only works for parentheses, square braces, curly braces and greater than and lesser than signs, but that’s all I need. Binding the function to % character on my modal key-map achieves exactly what I felt lacking after parting with evil-mode. Hope it could be of use to someone else as well.

Happy hacking!