;;; easy-escape.el --- Improve readability of escape characters in regular expressions -*- lexical-binding: t; -*- ;; Copyright (C) 2015, 2016, 2021 Free Software Foundation, Inc. ;; Author: Clément Pit-Claudel ;; Version: 0.2.1 ;; Keywords: convenience, lisp, tools ;; URL: https://github.com/cpitclaudel/easy-escape ;; 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 . ;;; Commentary: ;; `easy-escape-minor-mode' uses syntax highlighting and composition to make ELisp regular ;; expressions more readable. More precisely, it hides double backslashes ;; preceding regexp specials (`()|'), composes other double backslashes into ;; single ones, and applies a special face to each. The underlying buffer text ;; is not modified. ;; ;; For example, `easy-escape` prettifies this: ;; "\\(?:\\_<\\\\newcommand\\_>\\s-*\\)?" ;; into this (`^' indicates a different color): ;; "(?:\_<\\newcommand\_>\s-*)?". ;; ^ ^ ;; ;; The default is to use a single \ character instead of two, and to hide ;; backslashes preceding parentheses or `|'. The escape character and its color ;; can be customized using `easy-escape-face' and `easy-escape-character' (which ;; see), and backslashes before ()| can be shown by disabling ;; `easy-escape-hide-escapes-before-delimiters'. ;; ;; Suggested setup: ;; (add-hook 'lisp-mode-hook 'easy-escape-minor-mode) ;; ;; NOTE: If you find the distinction between the fontified double-slash and the ;; single slash too subtle, try the following: ;; ;; * Adjust the foreground of `easy-escape-face' ;; * Set `easy-escape-character' to a different character. ;;; Code: (require 'font-lock) (defgroup easy-escape nil "Improve readability of escape characters." :group 'programming) (defface easy-escape-face '((t :weight bold)) "Face used to highlight \\\\ in strings." :group 'easy-escape) (defface easy-escape-delimiter-face '((t :weight bold :slant normal :inherit font-lock-warning-face)) "Face used to highlight groups and alternations in strings." :group 'easy-escape) (defcustom easy-escape-character ?\\ "Character by which \\\\ is replaced when `easy-escape-minor-mode' is active. Good candidates include the following: \\ REVERSE SOLIDUS (the default, typed as '?\\\\') ╲ BOX DRAWINGS LIGHT DIAGONAL UPPER LEFT TO LOWER RIGHT (typed as '?╲') ⟍ MATHEMATICAL FALLING DIAGONAL (typed as '?⟍') ⑊ OCR DOUBLE BACKSLASH (typed as '?⑊') ⤡ NORTH WEST AND SOUTH EAST ARROW (typed as '?⤡') ↘ SOUTH EAST ARROW (typed as '?↘') ⇘ SOUTH EAST DOUBLE ARROW (typed as '?⇘') ⦥ MATHEMATICAL REVERSED ANGLE WITH UNDERBAR (typed as '?⦥') ⦣ MATHEMATICAL REVERSED ANGLE (typed as '?⦣') ⧹ BIG REVERSE SOLIDUS (typed as '?⧹') Most of these characters require non-standard fonts to display properly, however." :group 'easy-escape :type 'character) (defcustom easy-escape-hide-escapes-before-delimiters t "Whether to hide \\\\ when it precedes one of `(', `|', and `)'." :group 'easy-escape :type 'boolean) ;; FIXME use ppss? (defun easy-escape--in-string-p (pos) "Indicate whether POS is inside a string." (let ((face (get-text-property pos 'face))) (or (eq 'font-lock-doc-face face) (eq 'font-lock-string-face face) (and (listp face) (or (memq 'font-lock-doc-face face) (memq 'font-lock-string-face face)))))) (defun easy-escape--find-in-string (re lim) "Find next match for RE before LIM that falls in a string." (catch 'found (while (re-search-forward re lim t) (when (easy-escape--in-string-p (match-beginning 0)) (throw 'found t))))) (defun easy-escape--find-escape (limit) "Search for \\\\ before LIMIT." (easy-escape--find-in-string "\\(\\\\\\\\\\)" limit)) (defun easy-escape--find-delim (limit) "Search for a delimiter or alternation before LIMIT." (easy-escape--find-in-string "\\(\\\\\\\\\\)\\([()|]\\)" limit)) (defun easy-escape--compose (n char) "Compose match group N into CHAR." (compose-region (match-beginning n) (match-end n) char)) (defconst easy-escape--keywords '((easy-escape--find-escape (0 (progn (easy-escape--compose 0 easy-escape-character) 'easy-escape-face) prepend)) (easy-escape--find-delim (0 (progn (easy-escape--compose 0 (char-after (match-beginning 2))) 'easy-escape-delimiter-face) prepend))) "Font-lock keyword list used internally.") ;;;###autoload (define-minor-mode easy-escape-minor-mode "Compose escape signs together to make regexps more readable. When this mode is active, \\\\ in strings is displayed as a single \\, fontified using `easy-escape-face' and composed into `easy-escape-character'. If you find the distinction between the fontified double-slash and the single slash too subtle, try the following: * Adjust the foreground of `easy-escape-face' * Set `easy-escape-character' to a different character." :lighter " ez-esc" :group 'easy-escape (cond (easy-escape-minor-mode (font-lock-add-keywords nil easy-escape--keywords) (make-local-variable 'font-lock-extra-managed-props) (add-to-list 'font-lock-extra-managed-props 'composition)) (t (font-lock-remove-keywords nil easy-escape--keywords))) (if (>= emacs-major-version 25) (font-lock-flush) (with-no-warnings (font-lock-fontify-buffer)))) (provide 'easy-escape) ;;; easy-escape.el ends here