312 lines
13 KiB
EmacsLisp
312 lines
13 KiB
EmacsLisp
;;; rainbow-delimiters.el --- Highlight brackets according to their depth -*- lexical-binding: t -*-
|
|
|
|
;; Copyright (C)
|
|
;; 2010-2013 Jeremy Rayman
|
|
;; 2013-2021 Fanael Linithien
|
|
;; Author: Jeremy Rayman <opensource@jeremyrayman.com>
|
|
;; Fanael Linithien <fanael4@gmail.com>
|
|
;; Maintainer: Fanael Linithien <fanael4@gmail.com>
|
|
;; Created: 2010-09-02
|
|
;; Version: 2.1.5
|
|
;; Package-Version: 20210515.1254
|
|
;; Package-Commit: 7919681b0d883502155d5b26e791fec15da6aeca
|
|
;; Keywords: faces, convenience, lisp, tools
|
|
;; Homepage: https://github.com/Fanael/rainbow-delimiters
|
|
|
|
;; Note: despite `lexical-binding', there's no Emacs 24 dependency.
|
|
;; This is merely an optimization for Emacs 24+, the code is supposed to work
|
|
;; with *both* dynamic and lexical binding.
|
|
|
|
;; This program is free software; you can redistribute it and/or modify
|
|
;; it under the terms of the 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
|
|
;; GNU General Public License for more details.
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
;;; Installation:
|
|
|
|
;; The recommended way is to use MELPA (http://melpa.org/) or MELPA Stable
|
|
;; (http://stable.melpa.org/). If either is in your `package-archives', do
|
|
;; M-x package-install RET rainbow-delimiters RET
|
|
;; Otherwise, open `rainbow-delimiters.el' in Emacs and use
|
|
;; M-x package-install-from-buffer
|
|
;; Any other methods of installation are unsupported.
|
|
|
|
;;; Commentary:
|
|
;;
|
|
;; Rainbow-delimiters is a "rainbow parentheses"-like mode which highlights
|
|
;; parentheses, brackets, and braces according to their depth. Each
|
|
;; successive level is highlighted in a different color. This makes it easy
|
|
;; to spot matching delimiters, orient yourself in the code, and tell which
|
|
;; statements are at a given level.
|
|
;;
|
|
;; Great care has been taken to make this mode fast. You shouldn't see
|
|
;; any discernible change in scrolling or editing speed while using it,
|
|
;; even in delimiter-rich languages like Clojure, Lisp, and Scheme.
|
|
;;
|
|
;; Usage:
|
|
;;
|
|
;; To toggle the mode in the current buffer:
|
|
;; M-x rainbow-delimiters-mode
|
|
;; To start the mode automatically in `foo-mode', add the following to your init
|
|
;; file:
|
|
;; (add-hook 'foo-mode-hook #'rainbow-delimiters-mode)
|
|
;; To start the mode automatically in most programming modes (Emacs 24 and
|
|
;; above):
|
|
;; (add-hook 'prog-mode-hook #'rainbow-delimiters-mode)
|
|
;;
|
|
;; Customization:
|
|
;;
|
|
;; To customize various options, including the color theme:
|
|
;; M-x customize-group rainbow-delimiters
|
|
;;
|
|
;; You can specify custom colors by customizing following faces:
|
|
;; - Faces take the form `rainbow-delimiters-depth-N-face', with N being the
|
|
;; depth. Depth begins at 1, the outermost color. Faces exist for depths 1-9.
|
|
;; - The unmatched delimiter face: `rainbow-delimiters-unmatched-face'.
|
|
;; - The mismatched delimiter face: `rainbow-delimiters-mismatched-face'.
|
|
|
|
;;; Code:
|
|
|
|
(defgroup rainbow-delimiters nil
|
|
"Highlight nested parentheses, brackets, and braces according to their depth."
|
|
:prefix "rainbow-delimiters-"
|
|
:link '(url-link :tag "Website for rainbow-delimiters"
|
|
"https://github.com/Fanael/rainbow-delimiters")
|
|
:group 'applications)
|
|
|
|
(defgroup rainbow-delimiters-faces nil
|
|
"Faces for successively nested pairs of delimiters.
|
|
|
|
When depth exceeds innermost defined face, colors cycle back through."
|
|
:group 'rainbow-delimiters
|
|
:group 'faces
|
|
:link '(custom-group-link "rainbow-delimiters")
|
|
:prefix "rainbow-delimiters-")
|
|
|
|
(defcustom rainbow-delimiters-pick-face-function
|
|
#'rainbow-delimiters-default-pick-face
|
|
"The function used to pick a face used to highlight a delimiter.
|
|
The function should take three arguments (DEPTH MATCH LOC), where:
|
|
- DEPTH is the delimiter depth; when zero or negative, it's an unmatched
|
|
delimiter.
|
|
- MATCH is nil iff the delimiter is a mismatched closing delimiter.
|
|
- LOC is the location of the delimiter.
|
|
The function should return a value suitable to use as a value of the `face' text
|
|
property, or nil, in which case the delimiter is not highlighted.
|
|
The function should not move the point or mark or change the match data."
|
|
:tag "Pick face function"
|
|
:type 'function
|
|
:group 'rainbow-delimiters)
|
|
|
|
(defface rainbow-delimiters-base-face
|
|
'((default (:inherit unspecified)))
|
|
"Face inherited by all other rainbow-delimiter faces."
|
|
:group 'rainbow-delimiters-faces)
|
|
|
|
(defface rainbow-delimiters-base-error-face
|
|
'((default (:inherit rainbow-delimiters-base-face))
|
|
(t (:foreground "#88090B")))
|
|
"Face inherited by all other rainbow-delimiter error faces."
|
|
:group 'rainbow-delimiters-faces)
|
|
|
|
(defface rainbow-delimiters-unmatched-face
|
|
'((default (:inherit rainbow-delimiters-base-error-face)))
|
|
"Face to highlight unmatched closing delimiters in."
|
|
:group 'rainbow-delimiters-faces)
|
|
|
|
(defface rainbow-delimiters-mismatched-face
|
|
'((default (:inherit rainbow-delimiters-unmatched-face)))
|
|
"Face to highlight mismatched closing delimiters in."
|
|
:group 'rainbow-delimiters-faces)
|
|
|
|
(eval-when-compile
|
|
(defmacro rainbow-delimiters--define-depth-faces ()
|
|
(let ((faces '())
|
|
(light-colors ["#707183" "#7388d6" "#909183" "#709870" "#907373"
|
|
"#6276ba" "#858580" "#80a880" "#887070"])
|
|
(dark-colors ["grey55" "#93a8c6" "#b0b1a3" "#97b098" "#aebed8"
|
|
"#b0b0b3" "#90a890" "#a2b6da" "#9cb6ad"]))
|
|
(dotimes (i 9)
|
|
(push `(defface ,(intern (format "rainbow-delimiters-depth-%d-face" (1+ i)))
|
|
'((default (:inherit rainbow-delimiters-base-face))
|
|
(((class color) (background light)) :foreground ,(aref light-colors i))
|
|
(((class color) (background dark)) :foreground ,(aref dark-colors i)))
|
|
,(format "Nested delimiter face, depth %d." (1+ i))
|
|
:group 'rainbow-delimiters-faces)
|
|
faces))
|
|
`(progn ,@faces))))
|
|
(rainbow-delimiters--define-depth-faces)
|
|
|
|
(defcustom rainbow-delimiters-max-face-count 9
|
|
"Number of faces defined for highlighting delimiter levels.
|
|
|
|
Determines depth at which to cycle through faces again.
|
|
|
|
It's safe to change this variable provided that for all integers from 1 to the
|
|
new value inclusive, a face `rainbow-delimiters-depth-N-face' is defined."
|
|
:type 'integer
|
|
:group 'rainbow-delimiters)
|
|
|
|
(defcustom rainbow-delimiters-outermost-only-face-count 0
|
|
"Number of faces to be used only for N outermost delimiter levels.
|
|
|
|
This should be smaller than `rainbow-delimiters-max-face-count'."
|
|
:type 'integer
|
|
:group 'rainbow-delimiters)
|
|
|
|
|
|
(defun rainbow-delimiters-default-pick-face (depth match _loc)
|
|
"Return a face name appropriate for nesting depth DEPTH.
|
|
DEPTH and MATCH are as in `rainbow-delimiters-pick-face-function'.
|
|
|
|
The returned value is either `rainbow-delimiters-unmatched-face',
|
|
`rainbow-delimiters-mismatched-face', or one of the
|
|
`rainbow-delimiters-depth-N-face' faces, obeying
|
|
`rainbow-delimiters-max-face-count' and
|
|
`rainbow-delimiters-outermost-only-face-count'."
|
|
(cond
|
|
((<= depth 0)
|
|
'rainbow-delimiters-unmatched-face)
|
|
((not match)
|
|
'rainbow-delimiters-mismatched-face)
|
|
(t
|
|
(intern-soft
|
|
(concat "rainbow-delimiters-depth-"
|
|
(number-to-string
|
|
(if (<= depth rainbow-delimiters-max-face-count)
|
|
;; Our nesting depth has a face defined for it.
|
|
depth
|
|
;; Deeper than # of defined faces; cycle back through to
|
|
;; `rainbow-delimiters-outermost-only-face-count' + 1.
|
|
;; Return face # that corresponds to current nesting level.
|
|
(+ 1 rainbow-delimiters-outermost-only-face-count
|
|
(mod (- depth rainbow-delimiters-max-face-count 1)
|
|
(- rainbow-delimiters-max-face-count
|
|
rainbow-delimiters-outermost-only-face-count)))))
|
|
"-face")))))
|
|
|
|
(defun rainbow-delimiters--apply-color (loc depth match)
|
|
"Highlight a single delimiter at LOC according to DEPTH.
|
|
|
|
LOC is the location of the character to add text properties to.
|
|
DEPTH is the nested depth at LOC, which determines the face to use.
|
|
MATCH is nil iff it's a mismatched closing delimiter."
|
|
(let ((face (funcall rainbow-delimiters-pick-face-function depth match loc)))
|
|
(when face
|
|
(font-lock-prepend-text-property loc (1+ loc) 'face face))))
|
|
|
|
(defun rainbow-delimiters--char-ineligible-p (loc ppss delim-syntax-code)
|
|
"Return t if char at LOC should not be highlighted.
|
|
PPSS is the `parse-partial-sexp' state at LOC.
|
|
DELIM-SYNTAX-CODE is the `car' of a raw syntax descriptor at LOC.
|
|
|
|
Returns t if char at loc meets one of the following conditions:
|
|
- Inside a string.
|
|
- Inside a comment.
|
|
- Is an escaped char, e.g. ?\)"
|
|
(or
|
|
(nth 3 ppss) ; inside string?
|
|
(nth 4 ppss) ; inside comment?
|
|
(nth 5 ppss) ; escaped according to the syntax table?
|
|
;; Note: no need to consider single-char openers, they're already handled
|
|
;; by looking at ppss.
|
|
(cond
|
|
;; Two character opener, LOC at the first character?
|
|
((/= 0 (logand #x10000 delim-syntax-code))
|
|
(/= 0 (logand #x20000 (or (car (syntax-after (1+ loc))) 0))))
|
|
;; Two character opener, LOC at the second character?
|
|
((/= 0 (logand #x20000 delim-syntax-code))
|
|
(/= 0 (logand #x10000 (or (car (syntax-after (1- loc))) 0))))
|
|
(t
|
|
nil))))
|
|
|
|
;; Main function called by font-lock.
|
|
(defun rainbow-delimiters--propertize (end)
|
|
"Highlight delimiters in region between point and END.
|
|
|
|
Used by font-lock for dynamic highlighting."
|
|
(when (bound-and-true-p mmm-current-submode)
|
|
;; `mmm-mode' is weird and apparently needs this hack, because otherwise we
|
|
;; may end up thinking matched parentheses are mismatched.
|
|
(widen))
|
|
(let* ((last-ppss-pos (point))
|
|
(ppss (syntax-ppss)))
|
|
(while (> end (progn (skip-syntax-forward "^()" end)
|
|
(point)))
|
|
(let* ((delim-pos (point))
|
|
(delim-syntax (syntax-after delim-pos)))
|
|
(setq ppss (parse-partial-sexp last-ppss-pos delim-pos nil nil ppss))
|
|
(setq last-ppss-pos delim-pos)
|
|
;; `skip-syntax-forward' leaves the point at the delimiter, move past
|
|
;; it.
|
|
(forward-char)
|
|
(let ((delim-syntax-code (car delim-syntax)))
|
|
(cond
|
|
((rainbow-delimiters--char-ineligible-p delim-pos ppss delim-syntax-code)
|
|
nil)
|
|
((= 4 (logand #xFFFF delim-syntax-code))
|
|
;; The (1+ ...) is needed because `parse-partial-sexp' returns the
|
|
;; depth at the opening delimiter, not in the block being started.
|
|
(rainbow-delimiters--apply-color delim-pos (1+ (nth 0 ppss)) t))
|
|
(t
|
|
;; Not an opening delimiter, so it's a closing delimiter.
|
|
(let ((matches-p (eq (cdr delim-syntax) (char-after (nth 1 ppss)))))
|
|
(rainbow-delimiters--apply-color delim-pos (nth 0 ppss) matches-p))))))))
|
|
;; We already fontified the delimiters, tell font-lock there's nothing more
|
|
;; to do.
|
|
nil)
|
|
|
|
;; NB: no face defined here because we apply the faces ourselves instead of
|
|
;; leaving that to font-lock.
|
|
(defconst rainbow-delimiters--font-lock-keywords
|
|
'(rainbow-delimiters--propertize))
|
|
|
|
;;;###autoload
|
|
(define-minor-mode rainbow-delimiters-mode
|
|
"Highlight nested parentheses, brackets, and braces according to their depth."
|
|
:init-value nil
|
|
:lighter "" ; No modeline lighter - it's already obvious when the mode is on.
|
|
:keymap nil
|
|
(font-lock-remove-keywords nil rainbow-delimiters--font-lock-keywords)
|
|
(when rainbow-delimiters-mode
|
|
(font-lock-add-keywords nil rainbow-delimiters--font-lock-keywords 'append)
|
|
(set (make-local-variable 'jit-lock-contextually) t)
|
|
(when (or (bound-and-true-p syntax-begin-function)
|
|
(bound-and-true-p font-lock-beginning-of-syntax-function))
|
|
;; We're going to modify `syntax-begin-function', so flush the cache to
|
|
;; avoid getting cached values that used the old value.
|
|
(syntax-ppss-flush-cache 0))
|
|
;; `syntax-begin-function' may break the assumption we rely on that
|
|
;; `syntax-ppss' is exactly equivalent to `parse-partial-sexp' from
|
|
;; `point-min'. Just don't use it, the performance hit should be negligible.
|
|
(when (boundp 'syntax-begin-function)
|
|
(set (make-local-variable 'syntax-begin-function) nil))
|
|
;; Obsolete equivalent of `syntax-begin-function'.
|
|
(when (boundp 'font-lock-beginning-of-syntax-function)
|
|
(set (make-local-variable 'font-lock-beginning-of-syntax-function) nil)))
|
|
(when font-lock-mode
|
|
(if (fboundp 'font-lock-flush)
|
|
(font-lock-flush)
|
|
(with-no-warnings (font-lock-fontify-buffer)))))
|
|
|
|
;;;###autoload
|
|
(defun rainbow-delimiters-mode-enable ()
|
|
"Enable `rainbow-delimiters-mode'."
|
|
(rainbow-delimiters-mode 1))
|
|
|
|
;;;###autoload
|
|
(defun rainbow-delimiters-mode-disable ()
|
|
"Disable `rainbow-delimiters-mode'."
|
|
(rainbow-delimiters-mode 0))
|
|
|
|
(provide 'rainbow-delimiters)
|
|
;;; rainbow-delimiters.el ends here
|