dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

commit 1903acd6ca0d94a8eb9dab6da83ebf624d9bfaa8
parent 3e10447f8945623d8192a6d3d04e4092624aa81a
Author: Lukas Henkel <lh@entf.net>
Date:   Sat, 25 Nov 2023 12:29:38 +0100

Try out alternative lisp highlighting

Diffstat:
Aelpa/highlight-function-calls-20230416.445/highlight-function-calls-autoloads.el | 38++++++++++++++++++++++++++++++++++++++
Aelpa/highlight-function-calls-20230416.445/highlight-function-calls-pkg.el | 14++++++++++++++
Aelpa/highlight-function-calls-20230416.445/highlight-function-calls.el | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelpa/prism-0.3.2/prism-autoloads.el | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelpa/prism-0.3.2/prism-pkg.el | 15+++++++++++++++
Aelpa/prism-0.3.2/prism.el | 1211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minit.el | 33++++++++++++++++++++++++++++++---
7 files changed, 1552 insertions(+), 3 deletions(-)

diff --git a/elpa/highlight-function-calls-20230416.445/highlight-function-calls-autoloads.el b/elpa/highlight-function-calls-20230416.445/highlight-function-calls-autoloads.el @@ -0,0 +1,38 @@ +;;; highlight-function-calls-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- +;; Generated by the `loaddefs-generate' function. + +;; This file is part of GNU Emacs. + +;;; Code: + +(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) + + + +;;; Generated autoloads from highlight-function-calls.el + +(autoload 'highlight-function-calls-mode "highlight-function-calls" "\ +Highlight function calls. + +Toggle highlighting of function calls on or off. + +With a prefix argument ARG, enable if ARG is positive, and +disable it otherwise. If called from Lisp, enable the mode if ARG +is omitted or nil, and toggle it if ARG is `toggle'. + +(fn &optional ARG)" t) +(register-definition-prefixes "highlight-function-calls" '("highlight-function-calls-")) + +;;; End of scraped data + +(provide 'highlight-function-calls-autoloads) + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; no-native-compile: t +;; coding: utf-8-emacs-unix +;; End: + +;;; highlight-function-calls-autoloads.el ends here diff --git a/elpa/highlight-function-calls-20230416.445/highlight-function-calls-pkg.el b/elpa/highlight-function-calls-20230416.445/highlight-function-calls-pkg.el @@ -0,0 +1,14 @@ +(define-package "highlight-function-calls" "20230416.445" "Highlight function/macro calls" + '((emacs "24.4")) + :commit "e2ed2da188aea5879b59ffffefdc5eca10e7ba83" :authors + '(("Adam Porter" . "adam@alphapapa.net")) + :maintainers + '(("Adam Porter" . "adam@alphapapa.net")) + :maintainer + '("Adam Porter" . "adam@alphapapa.net") + :keywords + '("faces" "highlighting") + :url "http://github.com/alphapapa/highlight-function-calls") +;; Local Variables: +;; no-byte-compile: t +;; End: diff --git a/elpa/highlight-function-calls-20230416.445/highlight-function-calls.el b/elpa/highlight-function-calls-20230416.445/highlight-function-calls.el @@ -0,0 +1,172 @@ +;;; highlight-function-calls.el --- Highlight function/macro calls -*- lexical-binding: t; -*- + +;; Author: Adam Porter <adam@alphapapa.net> +;; Url: http://github.com/alphapapa/highlight-function-calls +;; Version: 0.1-pre +;; Package-Requires: ((emacs "24.4")) +;; Keywords: faces, highlighting + +;;; Commentary: + +;; This package highlights function symbols in function calls. This +;; makes them stand out from other symbols, which makes it easy to see +;; where calls to other functions are. Optionally, macros and special +;; forms can be highlighted as well. Also, a list of symbols can be +;; excluded from highlighting; by default, ones like +/-, </>, error, +;; require, etc. are excluded. Finally, the `not' function can be +;; highlighted specially. + +;; Just run `highlight-function-calls-mode' to activate, or you can +;; add that to your `emacs-lisp-mode-hook' to do it automatically. + +;;; License: + +;; 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/>. + +;;; Code: + +(defgroup highlight-function-calls nil + "Options for highlighting function/macro calls and special forms." + :group 'faces) + +(defface highlight-function-calls-face + '((t (:underline t))) + "Face for highlighting function calls." + :group 'highlight-function-calls) + +(defface highlight-function-calls--not-face + '((t (:inherit font-lock-negation-char-face))) + "Face for highlighting `not'." + :group 'highlight-function-calls) + +(defcustom highlight-function-calls-exclude-symbols + '( + = + + + - + / + * + < + > + <= + >= + debug ; Not intended as an interactive function + error + provide + require + signal + throw + user-error + ) + "List of symbols to not highlight." + :type '(repeat symbol)) + +(defcustom highlight-function-calls-macro-calls nil + "Whether to highlight macro calls." + :type 'boolean) + +(defcustom highlight-function-calls-special-forms nil + "Whether to highlight special-forms calls." + :type 'boolean) + +(defcustom highlight-function-calls-not nil + "Whether to highlight `not'." + :type 'boolean) + +(defconst highlight-function-calls--keywords + '(( + ;; First we match an opening paren, which prevents matching + ;; function names as arguments. We also avoid matching opening + ;; parens immediately after quotes. + + ;; FIXME: This does not avoid matching opening parens in quoted + ;; lists. I don't know if we can fix this, because `syntax-ppss' + ;; doesn't give any information about this. It might require + ;; using semantic, which we probably don't want to mess with. + + ;; FIXME: It also doesn't avoid matching, e.g. the `map' in "(let + ;; ((map". I'm not sure why. + "\\(?:^\\|[[:space:]]+\\|,\\)(" ; (rx (or bol (1+ space) ",") "(") + + ;; NOTE: The (0 nil) is required, although I don't understand + ;; exactly why. This was confusing enough, following the + ;; docstring for `font-lock-add-keywords'. + (0 nil) + + ;; Now we use a HIGHLIGHT MATCH-ANCHORED form to match the symbol + ;; after the paren. We call the `highlight-function-calls--matcher' + ;; function to test whether the face should be applied. We use a + ;; PRE-MATCH-FORM to return a position at the end of the symbol, + ;; which prevents matching function name symbols later on the + ;; line, but we must not move the point in the process. We do + ;; not use a POST-MATCH-FORM. Then we use the MATCH-HIGHLIGHT + ;; form to highlight group 0, which is the whole symbol, we apply + ;; the `highlight-function-calls-face' face, and we `prepend' it so + ;; that it overrides existing faces; this way we even work with, + ;; e.g. `rainbow-identifiers-mode', but only if we're activated + ;; last. + (highlight-function-calls--matcher + (save-excursion + (forward-symbol 1) + (point)) + nil + (0 highlight-function-calls--face-name prepend)))) + "Keywords argument for `font-lock-add-keywords'.") + +(defvar highlight-function-calls--face-name nil) + +(defun highlight-function-calls--matcher (end) + "The matcher function to be used by font lock mode." + (setq end (save-excursion (forward-symbol 1) (point))) + (catch 'highlight-function-calls--matcher + (when (not (nth 5 (syntax-ppss))) + (while (re-search-forward (rx symbol-start (*? any) symbol-end) end t) + (let ((match (intern-soft (match-string 0)))) + (when (and (or (functionp match) + (when highlight-function-calls-macro-calls + (macrop match)) + (when highlight-function-calls-special-forms + (special-form-p match))) + (not (member match highlight-function-calls-exclude-symbols))) + (goto-char (match-end 0)) + (setq highlight-function-calls--face-name + (pcase match + ((and (or 'not 'null) (guard highlight-function-calls-not)) 'highlight-function-calls--not-face) + (_ 'highlight-function-calls-face))) + (throw 'highlight-function-calls--matcher t))))) + nil)) + +;;;###autoload +(define-minor-mode highlight-function-calls-mode + "Highlight function calls. + +Toggle highlighting of function calls on or off. + +With a prefix argument ARG, enable if ARG is positive, and +disable it otherwise. If called from Lisp, enable the mode if ARG +is omitted or nil, and toggle it if ARG is `toggle'." + :init-value nil :lighter nil :keymap nil + (let ((keywords highlight-function-calls--keywords)) + (font-lock-remove-keywords nil keywords) + (when highlight-function-calls-mode + (font-lock-add-keywords nil keywords 'append))) + ;; Refresh font locking. + (when font-lock-mode + (if (fboundp 'font-lock-flush) + (font-lock-flush) + (with-no-warnings (font-lock-fontify-buffer))))) + +(provide 'highlight-function-calls) + +;;; highlight-function-calls.el ends here diff --git a/elpa/prism-0.3.2/prism-autoloads.el b/elpa/prism-0.3.2/prism-autoloads.el @@ -0,0 +1,72 @@ +;;; prism-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- +;; Generated by the `loaddefs-generate' function. + +;; This file is part of GNU Emacs. + +;;; Code: + +(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) + + + +;;; Generated autoloads from prism.el + +(autoload 'prism-mode "prism" "\ +Disperse code into a spectrum of colors according to depth. + +Depth is determined by list nesting. Suitable for Lisp, C-like +languages, etc. + +This is a minor mode. If called interactively, toggle the `Prism +mode' mode. If the prefix argument is positive, enable the mode, +and if it is zero or negative, disable the mode. + +If called from Lisp, toggle the mode if ARG is `toggle'. Enable +the mode if ARG is nil, omitted, or is a positive number. +Disable the mode if ARG is a negative number. + +To check whether the minor mode is enabled in the current buffer, +evaluate `prism-mode'. + +The mode's hook is called both when the mode is enabled and when +it is disabled. + +(fn &optional ARG)" t) +(autoload 'prism-whitespace-mode "prism" "\ +Disperse code into a spectrum of colors according to depth. + +Depth is determined by indentation and list nesting. Suitable +for whitespace-sensitive languages like Python, Haskell, shell, +etc. + +This is a minor mode. If called interactively, toggle the +`Prism-Whitespace mode' mode. If the prefix argument is +positive, enable the mode, and if it is zero or negative, disable +the mode. + +If called from Lisp, toggle the mode if ARG is `toggle'. Enable +the mode if ARG is nil, omitted, or is a positive number. +Disable the mode if ARG is a negative number. + +To check whether the minor mode is enabled in the current buffer, +evaluate `prism-whitespace-mode'. + +The mode's hook is called both when the mode is enabled and when +it is disabled. + +(fn &optional ARG)" t) +(register-definition-prefixes "prism" '("prism-")) + +;;; End of scraped data + +(provide 'prism-autoloads) + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; no-native-compile: t +;; coding: utf-8-emacs-unix +;; End: + +;;; prism-autoloads.el ends here diff --git a/elpa/prism-0.3.2/prism-pkg.el b/elpa/prism-0.3.2/prism-pkg.el @@ -0,0 +1,15 @@ +(define-package "prism" "0.3.2" "Customizable, depth-based syntax coloring" + '((emacs "26.1") + (dash "2.14.1")) + :commit "169b49afa91e69d35b8756df49ed3ca06f418d35" :authors + '(("Adam Porter" . "adam@alphapapa.net")) + :maintainers + '(("Adam Porter" . "adam@alphapapa.net")) + :maintainer + '("Adam Porter" . "adam@alphapapa.net") + :keywords + '("faces" "lisp") + :url "https://github.com/alphapapa/prism.el") +;; Local Variables: +;; no-byte-compile: t +;; End: diff --git a/elpa/prism-0.3.2/prism.el b/elpa/prism-0.3.2/prism.el @@ -0,0 +1,1211 @@ +;;; prism.el --- Customizable, depth-based syntax coloring -*- lexical-binding: t; -*- + +;; Copyright (C) 2019 Adam Porter + +;; Author: Adam Porter <adam@alphapapa.net> +;; URL: https://github.com/alphapapa/prism.el +;; Version: 0.3.2 +;; Package-Requires: ((emacs "26.1") (dash "2.14.1")) +;; Keywords: faces lisp + +;;; License: + +;; 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 <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; `prism' disperses Lisp forms (and other syntax bounded by +;; parentheses, brackets, and braces) into a spectrum of color by +;; depth. It's similar to `rainbow-blocks', but it respects existing +;; non-color face properties, and allows flexible configuration of +;; faces and colors. It also optionally colorizes strings and/or +;; comments by code depth in a similar, customizable way. + +;; Usage: + +;; 1. Run the appropriate command for the current buffer: + +;; - For Lisp and C-like languages, use `prism-mode'. + +;; - For significant-whitespace languages like Python, or ones whose +;; depth is not always indicated by parenthetical characters, like +;; shell, use `prism-whitespace-mode' instead. + +;; 2. Enjoy. + +;; When a theme is loaded or disabled, colors are automatically +;; updated. + +;; To customize, see the `prism' customization group, e.g. by using +;; "M-x customize-group RET prism RET". For example, by default, +;; comments and strings are colorized according to depth, similarly to +;; code, but this can be disabled. + +;; Advanced: + +;; More advanced customization of faces is done by calling +;; `prism-set-colors', which can override the default settings and +;; perform additional color manipulations. The primary argument is +;; COLORS, which should be a list of colors, each of which may be a +;; name, a hex RGB string, or a face name (of which the foreground +;; color is used). Note that the list of colors need not be as long +;; as the number of faces that's actually set (e.g. the default is 16 +;; faces), because the colors are automatically repeated and adjusted +;; as necessary. + +;; If `prism-set-colors' is called with the SAVE argument, the results +;; are saved to customization options so that `prism-mode' will use +;; those colors by default. + +;; Here's an example that the author finds pleasant: + +;; (prism-set-colors :num 16 +;; :desaturations (cl-loop for i from 0 below 16 +;; collect (* i 2.5)) +;; :lightens (cl-loop for i from 0 below 16 +;; collect (* i 2.5)) +;; :colors (list "sandy brown" "dodgerblue" "medium sea green") +;; +;; :comments-fn +;; (lambda (color) +;; (prism-blend color +;; (face-attribute 'font-lock-comment-face :foreground) 0.25)) +;; +;; :strings-fn +;; (lambda (color) +;; (prism-blend color "white" 0.5))) + +;;; Code: + +;;;; Requirements + +(require 'cl-lib) +(require 'color) +(require 'face-remap) +(require 'thingatpt) +(require 'subr-x) + +(require 'dash) + +;;;; Variables + +(defvar prism-faces nil + "Alist mapping depth levels to faces.") + +(defvar prism-faces-comments nil + "Alist mapping depth levels to string faces.") + +(defvar prism-faces-strings nil + "Alist mapping depth levels to string faces.") + +(defvar prism-faces-parens nil + "Alist mapping depth levels to parens faces.") + +(defvar prism-face nil + "Set by `prism-match' during fontification.") + +(defvar-local prism-syntax-table nil + "Syntax table used by `prism-mode'. +Set automatically.") + +(defvar-local prism-whitespace-indent-offset 4 + "Number of spaces which represents a semantic level of indentation. +Set automatically by `prism-whitespace-mode'. Should be set +appropriately for the current mode, e.g. `python-indent-offset' +for `python-mode'.") + +;; Defined as custom variables later in the file, but declared here to +;; silence the byte-compiler, because they're used in `prism-set-colors', +;; which is defined before their defcustoms. It's circular, but this +;; breaks the loop. +(defvar prism-colors) +(defvar prism-color-attribute) +(defvar prism-color-distance) +(defvar prism-desaturations) +(defvar prism-lightens) +(defvar prism-num-faces) +(defvar prism-comments-fn) +(defvar prism-comments) +(defvar prism-parens) +(defvar prism-parens-fn) +(defvar prism-strings-fn) +(defvar prism-strings) +(defvar prism-whitespace-mode-indents) + +;;;; Macros + +(defmacro prism-extrapolate (start times length form) + "Return list of numbers extrapolated from FORM. +Starting from number START, repeating below TIMES, collect the +value of FORM. Each iteration, `i' is bound to the iteration +number (the incremented value of START), and `c' is bound to the +number of the current cycle through LENGTH, starting at 1. + +For example, this form: + + (prism-extrapolate 0 24 3 (* c 3)) + +Evaluates to: + + (3 3 3 6 6 6 9 9 9 12 12 12 15 15 15 18 18 18 21 21 21 24 24 24) + +Intended for use as the DESATURATIONS and LIGHTENS arguments to +`prism-set-colors'." + `(cl-loop with c = 1 with reset = 1 + for i from ,start below ,times + collect ,form + do (if (= reset ,length) + (setf reset 1 + c (1+ c)) + (cl-incf reset)))) + +;; NOTE: Since this will likely be useful in the future, I'm leaving it in, commented. + +;; (cl-defmacro prism-debug (&rest args) +;; "Display a debug warning showing the runtime value of ARGS. +;; The warning automatically includes the name of the containing +;; function, and it is only displayed if `warning-minimum-log-level' +;; is `:debug' at runtime (which avoids formatting messages that +;; won't be shown). +;; +;; Each of ARGS may be a string, which is displayed as-is, or a +;; symbol, the value of which is displayed prefixed by its name, or +;; a Lisp form, which is displayed prefixed by its first symbol. +;; +;; Before the actual ARGS arguments, you can write keyword +;; arguments, i.e. alternating keywords and values. The following +;; keywords are supported: +;; +;; :buffer BUFFER Name of buffer to pass to `display-warning'. +;; :level LEVEL Level passed to `display-warning', which see. +;; Default is :debug." +;; (pcase-let* ((fn-name (with-current-buffer +;; (or byte-compile-current-buffer (current-buffer)) +;; ;; This is a hack, but a nifty one. +;; (save-excursion +;; (beginning-of-defun) +;; (cl-second (read (current-buffer)))))) +;; (plist-args (cl-loop while (keywordp (car args)) +;; collect (pop args) +;; collect (pop args))) +;; ((map (:buffer buffer) (:level level)) plist-args) +;; (level (or level :debug)) +;; (string (cl-loop for arg in args +;; concat (pcase arg +;; ((pred stringp) "%s ") +;; ((pred symbolp) +;; (concat (upcase (symbol-name arg)) ":%s ")) +;; ((pred listp) +;; (concat "(" (upcase (symbol-name (car arg))) +;; (pcase (length arg) +;; (1 ")") +;; (_ "...)")) +;; ":%s ")))))) +;; `(when (eq :debug warning-minimum-log-level) +;; (display-warning ',fn-name (format ,string ,@args) ,level ,buffer)))) + +;;;; Minor mode + +(defun prism-active-mode () + "Return any already-active `prism' modes in this buffer. +There should only ever be one, but the return value is a list of +modes." + (cl-loop for mode in '(prism-mode prism-whitespace-mode) + when (symbol-value mode) + collect mode)) + +;;;###autoload +(define-minor-mode prism-mode + "Disperse code into a spectrum of colors according to depth. +Depth is determined by list nesting. Suitable for Lisp, C-like +languages, etc." + :global nil + (let ((keywords '((prism-match 0 prism-face prepend)))) + (if prism-mode + (progn + (dolist (mode (cl-remove 'prism-mode (prism-active-mode))) + ;; Deactivate alternative mode so this one can be enabled. + (funcall mode -1)) + (unless prism-faces + (prism-set-colors)) + (setq prism-syntax-table (prism-syntax-table (syntax-table))) + (font-lock-add-keywords nil keywords 'append) + (font-lock-flush) + (add-hook 'font-lock-extend-region-functions #'prism-extend-region nil 'local) + (unless (advice-member-p #'prism-after-theme #'load-theme) + ;; Don't add the advice again, because this mode is + ;; buffer-local, but the advice is global. + (advice-add #'load-theme :after #'prism-after-theme) + (advice-add #'disable-theme :after #'prism-after-theme))) + (font-lock-remove-keywords nil keywords) + (prism-remove-faces) + (unless (--any (or (buffer-local-value 'prism-mode it) + (buffer-local-value 'prism-whitespace-mode it)) + (buffer-list)) + ;; Don't remove advice if `prism' is still active in any buffers. + (advice-remove #'load-theme #'prism-after-theme) + (advice-remove #'disable-theme #'prism-after-theme)) + (remove-hook 'font-lock-extend-region-functions #'prism-extend-region 'local) + (font-lock-flush)))) + +;;;###autoload +(define-minor-mode prism-whitespace-mode + "Disperse code into a spectrum of colors according to depth. +Depth is determined by indentation and list nesting. Suitable +for whitespace-sensitive languages like Python, Haskell, shell, +etc." + :global nil + (let ((keywords '((prism-match-whitespace 0 prism-face prepend)))) + (if prism-whitespace-mode + (progn + (dolist (mode (cl-remove 'prism-whitespace-mode (prism-active-mode))) + ;; Deactivate alternative mode so this one can be enabled. + (funcall mode -1)) + (unless prism-faces + (prism-set-colors)) + (setf prism-syntax-table (prism-syntax-table (syntax-table)) + prism-whitespace-indent-offset (let ((indent (or (alist-get major-mode prism-whitespace-mode-indents) + (alist-get t prism-whitespace-mode-indents)))) + (cl-etypecase indent + (symbol (symbol-value indent)) + (integer indent)))) + (font-lock-add-keywords nil keywords 'append) + (font-lock-flush) + (add-hook 'font-lock-extend-region-functions #'prism-extend-region nil 'local) + (unless (advice-member-p #'prism-after-theme #'load-theme) + ;; Don't add the advice again, because this mode is + ;; buffer-local, but the advice is global. + (advice-add #'load-theme :after #'prism-after-theme) + (advice-add #'disable-theme :after #'prism-after-theme))) + (font-lock-remove-keywords nil keywords) + (prism-remove-faces) + (unless (--any (or (buffer-local-value 'prism-mode it) + (buffer-local-value 'prism-whitespace-mode it)) + (buffer-list)) + ;; Don't remove advice if `prism' is still active in any buffers. + (advice-remove #'load-theme #'prism-after-theme) + (advice-remove #'disable-theme #'prism-after-theme)) + (remove-hook 'font-lock-extend-region-functions #'prism-extend-region 'local) + (font-lock-flush)))) + +;;;; Functions + +(defun prism-after-theme (&rest args) + "For `load-theme' advice. +ARGS may be what `load-theme' and `disable-theme' expect. Unless +NO-ENABLE (optional third argument, like `load-theme') is +non-nil, call `prism-set-colors' to update `prism' faces." + (unless (cl-third args) + (prism-set-colors))) + +;; Silence byte-compiler for these special variables that are bound +;; around `font-lock-extend-region-functions'. +(defvar font-lock-beg) +(defvar font-lock-end) + +(defun prism-extend-region () + "Extend region to the current sexp. +For `font-lock-extend-region-functions'." + ;; (prism-debug (current-buffer) (point) font-lock-beg font-lock-end) + (let (changed-p) + ;; NOTE: It doesn't seem to be necessary to extend the region backward/up, but I'm + ;; not completely sure that this is never needed, so I'm leaving it in, commented. + ;; (unless (= 0 (nth 0 (syntax-ppss))) + ;; ;; Not at top level: extend region backward/up. + ;; (let ((orig-pos (point))) + ;; (save-excursion + ;; (when (ignore-errors + ;; (backward-up-list 1 t t)) + ;; (setf font-lock-beg (point)) + ;; (unless (= font-lock-beg orig-pos) + ;; (setf changed-p t)))))) + (save-excursion + (goto-char font-lock-end) + (unless (= 0 (nth 0 (syntax-ppss))) + ;; Not at top level: extend region forward. + (let ((end (save-excursion + (when (ignore-errors + (backward-up-list -1 t t)) + (point))))) + (when (and end (> end font-lock-end)) + (setf font-lock-end (1- end) + changed-p t) + changed-p)))))) + +(defun prism-syntax-table (syntax-table) + "Return SYNTAX-TABLE modified for `prism'." + ;; Copied from `rainbow-blocks-make-syntax-table'. + (let ((table (copy-syntax-table syntax-table))) + (modify-syntax-entry ?\( "() " table) + (modify-syntax-entry ?\) ")( " table) + (modify-syntax-entry ?\[ "(]" table) + (modify-syntax-entry ?\] ")[" table) + (modify-syntax-entry ?\{ "(}" table) + (modify-syntax-entry ?\} "){" table) + table)) + +(defun prism-match (limit) + "Matcher function for `font-lock-keywords'. +Matches up to LIMIT." + ;; (prism-debug (current-buffer) (point) limit) + (cl-macrolet ((parse-syntax () + `(-setq (depth _ _ in-string-p comment-level-p _ _ _ comment-or-string-start) + (syntax-ppss))) + (comment-p () + ;; This macro should only be used after `parse-syntax'. + `(or comment-level-p (looking-at-p (rx (syntax comment-start))) + ;; Not all language modes' syntax tables seem to allow searching + ;; for comment-start, comment-end, or comment-delimiter + ;; characters, so we must use ppss to determine whether we're + ;; looking at a comment start. And since some languages use + ;; multiples of a character to mark a comment start (e.g. "//"), + ;; we must also test at 2 characters past the point. And since + ;; that position could be past the end of the buffer, we must + ;; ignore such an error. + (condition-case nil + (or (save-excursion + (ppss-comment-depth (syntax-ppss (1+ (point))))) + (save-excursion + (ppss-comment-depth (syntax-ppss (+ 2 (point)))))) + (args-out-of-range nil)))) + (looking-at-paren-p + () `(looking-at-p (rx (or (syntax open-parenthesis) + (syntax close-parenthesis))))) + (face-at () + ;; Return face to apply. Should be called with point at `start'. + `(cond ((and prism-parens (looking-at-paren-p)) + (alist-get depth prism-faces-parens)) + ((comment-p) + (pcase depth + (0 'font-lock-comment-face) + (_ (if prism-faces-comments + (alist-get depth prism-faces-comments) + (alist-get depth prism-faces))))) + ((or in-string-p (looking-at-p (rx (syntax string-quote)))) + (pcase depth + (0 'font-lock-string-face) + (_ (if prism-faces-strings + (alist-get depth prism-faces-strings) + (alist-get depth prism-faces))))) + (t (alist-get depth prism-faces))))) + (with-syntax-table prism-syntax-table + (catch 'eobp + (let ((parse-sexp-ignore-comments t) + (starting-pos (point)) + depth in-string-p comment-level-p comment-or-string-start start end + found-comment-p found-string-p) + (while ;; Skip to start of where we should match. + (cond ((eobp) + ;; Stop matching and return nil if at end-of-buffer. + (throw 'eobp nil)) + ((eolp) + (forward-line 1)) + ((looking-at-p (rx blank)) + (forward-whitespace 1)) + ((unless prism-strings + (when (looking-at-p (rx (syntax string-quote))) + ;; At a string: skip it. + (forward-sexp)))) + ((unless prism-comments + (forward-comment (buffer-size)))))) + (parse-syntax) + (when in-string-p + ;; In a string: go back to its beginning (before its delimiter). + ;; It would be nice to leave this out and rely on the check in + ;; the `while' above, but if partial fontification starts inside + ;; a string, we have to handle that. + ;; NOTE: If a string contains a Lisp comment (e.g. in + ;; `custom-save-variables'), `in-string-p' will be non-nil, but + ;; `comment-or-string-start' will be nil. I don't know if this + ;; is a bug in `parse-partial-sexp', but we have to handle it. + (when comment-or-string-start + (goto-char comment-or-string-start) + (unless prism-strings + (forward-sexp)) + (parse-syntax))) + ;; Set start and end positions. + (setf start (point) + ;; I don't know if `ignore-errors' is going to be slow, but since + ;; `scan-lists' and `scan-sexps' signal errors, it seems necessary if we want + ;; to use them (and they seem to be cleaner to use than regexp searches). + end (min limit + (save-excursion + (or (when (looking-at-p (rx (syntax close-parenthesis))) + ;; I'd like to just use `scan-lists', but I can't find a way + ;; around this initial check. The code (scan-lists start 1 + ;; 1), when called just inside a list, scans past the end of + ;; it, to just outside it, which is not what we want, because + ;; we want to highlight the closing paren with the shallower + ;; depth. But if we just back up one character, we never + ;; exit the list. So we have to check whether we're looking + ;; at the close of a list, and if so, move just past it. + (cl-decf depth) + (1+ start)) + (when (and prism-comments (comment-p)) + (when comment-or-string-start + (goto-char comment-or-string-start)) + (forward-comment (buffer-size)) + (setf found-comment-p t) + (point)) + (when (looking-at-p (rx (syntax string-quote))) + (if in-string-p + ;; At end of string: break out of it. + (forward-char 1) + ;; At beginning of string: skip it. + (condition-case err + (forward-sexp 1) + (scan-error + ;; An unclosed string: move past it. + (goto-char (cadddr err))))) + ;; TODO: Is it right to set found-string-p in + ;; the case of finding an unclosed string? + (setf found-string-p t) + (point)) + (ignore-errors + ;; Scan to the past the delimiter of the next deeper list. + (scan-lists start 1 -1)) + (ignore-errors + ;; Scan to the end of the current list delimiter. + (1- (scan-lists start 1 1))) + ;; If we can't find anything, return `limit'. I'm not sure if + ;; this is the correct thing to do, but it avoids an error (and + ;; possibly hanging Emacs) in the event of an undiscovered bug. + ;; Although, signaling an error might be better, because I have + ;; seen "redisplay" errors related to font-lock in the messages + ;; buffer before, which might mean that Emacs can handle that. + ;; I think the important thing is not to hang Emacs, to always + ;; either return nil or advance point to `limit'. + limit)) + (or (unless (or found-string-p found-comment-p) + ;; This additional form is regrettable, but it seems necessary + ;; to fix <https://github.com/alphapapa/prism.el/issues/18>. + ;; However, there might be a better way to refactor this whole + ;; calculation of the END position, so someday that should be + ;; tried. (Or maybe just use tree-sitter in Emacs 29+.) + (save-excursion + (when (re-search-forward (rx (or (syntax string-quote) + (syntax comment-start))) + (or (ignore-errors + (scan-lists (point) 1 1)) + limit) + t) + ;; Found string or comment in current list: stop at beginning of it. + (pcase (syntax-after (match-beginning 0)) + ('(11) + (setf found-comment-p t) + (match-beginning 0)) + (`(7 . ,_) + (setf found-string-p t) + (match-beginning 0)))))) + limit))) + (when (< end start) + ;; Set search bound properly when `start' is greater than + ;; `end' (i.e. when `start' is moved past `limit', I think). + (setf end start)) + (when end + ;; End found: Try to fontify. + (save-excursion + (or (unless (or in-string-p found-string-p found-comment-p) + ;; Neither in a string nor looking at nor in a + ;; comment: set `end' to any comment found before it. + (when (re-search-forward (rx (or (seq (not (syntax escape)) (syntax string-quote)) + (syntax comment-start))) + end t) + (unless (equal '(7) (syntax-after (match-beginning 0))) + ;; Not in a string: set end to the beginning + ;; of the comment (this avoids stopping at + ;; comment-starts inside strings). + (setf end (match-beginning 0))))) + (unless (or found-comment-p found-string-p) + ;; Neither in nor looking at a comment: set `end' + ;; to any string or comment found before it. + (when (re-search-forward (rx (syntax string-quote)) end t) + (setf end (match-beginning 0)))))) + (when prism-parens + (unless (= 1 (- end start)) + ;; Not fontifying a single open paren (i.e. we are trying to fontify more + ;; than just an open paren): so if we are looking at one, fontify only it. + (when (eq 4 (syntax-class (syntax-after (1- end)))) + ;; End is past an open paren: back up one character. + (cl-decf end)))) + (if (and (comment-p) (= 0 depth)) + (setf prism-face nil) + (setf prism-face (face-at))) + (goto-char end) + (unless (> (point) start) + ;; Prevent end-of-buffer error in `font-lock-fontify-keywords-region'. + (cl-decf start)) + (set-match-data (list start end (current-buffer))) + ;; (prism-debug (current-buffer) "END" start end) + ;; Be sure to return non-nil! + (unless (> (point) starting-pos) + (prism-mode -1) + (error "prism: Infinite loop detected in `prism-match' (buffer:%S point:%S). Please report this bug" + (current-buffer) (point))) + t)))))) + +(defun prism-match-whitespace (limit) + "Matcher function for `font-lock-keywords' in whitespace-sensitive buffers. +Matches up to LIMIT. Requires `prism-whitespace-indent-offset' be set +appropriately, e.g. to `python-indent-offset' for `python-mode'." + (cl-macrolet ((parse-syntax () + `(-setq (list-depth _ _ in-string-p comment-level-p _ _ _ comment-or-string-start) + (syntax-ppss))) + (indent-depth () + `(or (save-excursion + (forward-line -1) + (when (looking-at-p (rx (1+ nonl) "\\" eol)) + ;; Found backslask-continued line: move + ;; to where the continued line starts. + (cl-loop do (forward-line -1) + while (looking-at-p (rx (1+ nonl) "\\" eol))) + (forward-line 1) ; Yes, go back down a line. + (/ (current-indentation) prism-whitespace-indent-offset))) + (/ (current-indentation) prism-whitespace-indent-offset))) + (depth-at () + ;; Yes, this is entirely too complicated--just like Python's syntax in + ;; comparison to Lisp. But, "Eww, all those parentheses!" they say. + ;; Well, all those parentheses avoid lots of special cases like these. + `(pcase list-depth + (0 (cond ((looking-at-p (rx (syntax close-parenthesis) eol)) + (save-excursion + (forward-char 1) + (backward-sexp 1) + (+ (nth 0 (syntax-ppss)) (indent-depth)))) + ((looking-back (rx (syntax close-parenthesis)) (1- (point))) + (save-excursion + (backward-sexp 1) + (+ (nth 0 (syntax-ppss)) (indent-depth)))) + (t (indent-depth)))) + ;; This handles the case of code that is both enclosed in a + ;; character-delimited list and indented on a new line within that + ;; list to match the list's opening indentation (e.g. in Python, + ;; when an if's condition is parenthesized and split across lines). + (_ (let* ((current-depth (car (syntax-ppss))) ;; This `syntax-ppss' call *is* necessary! + (enclosing-list-depth + (pcase current-depth + (0 0) + (_ (save-excursion + ;; Escape current list and return the level of + ;; the enclosing list plus its indent depth. + + ;; FIXME: When a preceding comment contains an apostrophe, this + ;; call to `scan-lists' interprets the apostrophe as delimiting a + ;; list, and it skips back to another preceding apostrophe, even + ;; inside a different top-level form, which causes the wrong + ;; depth to be calculated. ... Well, good news, I guess: this + ;; happens on Emacs 26.3 but not on Emacs 27.1. I guess + ;; something was fixed, which means that it's not a bug in Prism. + (goto-char (scan-lists (point) -1 current-depth)) + (+ (indent-depth) (car (syntax-ppss)))))))) + (pcase enclosing-list-depth + (0 (+ list-depth (1- (indent-depth)))) + (_ (+ enclosing-list-depth list-depth))))))) + (comment-p () + ;; This macro should only be used after `parse-syntax'. + `(or comment-level-p (looking-at-p (rx (or (syntax comment-start) + (syntax comment-delimiter)))) + ;; Not all language modes' syntax tables seem to allow searching + ;; for comment-start, comment-end, or comment-delimiter + ;; characters, so we must use ppss to determine whether we're + ;; looking at a comment start. And since some languages use + ;; multiples of a character to mark a comment start (e.g. "//"), + ;; we must also test at 2 characters past the point. And since + ;; that position could be past the end of the buffer, we must + ;; ignore such an error. + (condition-case nil + (or (save-excursion + (ppss-comment-depth (syntax-ppss (1+ (point))))) + (save-excursion + (ppss-comment-depth (syntax-ppss (+ 2 (point)))))) + (args-out-of-range nil)))) + (face-at () + ;; Return face to apply. Should be called with point at `start'. + `(let ((depth (depth-at))) + (cond ((comment-p) + (pcase depth + (0 'font-lock-comment-face) + (_ (if prism-faces-comments + (alist-get depth prism-faces-comments) + (alist-get depth prism-faces))))) + ((or in-string-p (looking-at-p (rx (or (syntax string-quote) + (syntax string-delimiter))))) + (pcase depth + (0 'font-lock-string-face) + (_ (if prism-faces-strings + (alist-get depth prism-faces-strings) + (alist-get depth prism-faces))))) + (t (alist-get depth prism-faces)))))) + (with-syntax-table prism-syntax-table + (unless (eobp) + ;; Not at end-of-buffer: start matching. + (let ((parse-sexp-ignore-comments t) + (starting-pos (point)) + list-depth in-string-p comment-level-p comment-or-string-start start end + found-comment-p found-string-p) + (while ;; Skip to start of where we should match. + (and (not (eobp)) + (cond ((eolp) + (forward-line 1)) + ((looking-at-p (rx blank)) + (forward-whitespace 1)) + ((unless prism-strings + (when (looking-at-p (rx (syntax string-quote))) + ;; At a string: skip it. + (forward-sexp)))) + ((unless prism-comments + (forward-comment (buffer-size))))))) + (parse-syntax) + (when in-string-p + ;; In a string: go back to its beginning (before its delimiter). + ;; It would be nice to leave this out and rely on the check in + ;; the `while' above, but if partial fontification starts inside + ;; a string, we have to handle that. + ;; NOTE: If a string contains a Lisp comment (e.g. in + ;; `custom-save-variables'), `in-string-p' will be non-nil, but + ;; `comment-or-string-start' will be nil. I don't know if this + ;; is a bug in `parse-partial-sexp', but we have to handle it. + (when comment-or-string-start + (goto-char comment-or-string-start) + (unless prism-strings + (forward-sexp)) + (parse-syntax))) + ;; Set start and end positions. + (setf start (point) + ;; I don't know if `ignore-errors' is going to be slow, but since + ;; `scan-lists' and `scan-sexps' signal errors, it seems necessary if we want + ;; to use them (and they seem to be cleaner to use than regexp searches). + end (min limit + (save-excursion + (or (when (and prism-comments (comment-p)) + (setf found-comment-p t) + (when comment-or-string-start + (goto-char comment-or-string-start)) + ;; We must only skip one comment, because before there is + ;; non-comment, non-whitespace text, the indent depth might change. + (forward-comment 1) + (point)) + (when (looking-at-p (rx (syntax close-parenthesis))) + ;; I'd like to just use `scan-lists', but I can't find a way around this initial check. + ;; The code (scan-lists start 1 1), when called just inside a list, scans past the end + ;; of it, to just outside it, which is not what we want, because we want to highlight + ;; the closing paren with the shallower depth. But if we just back up one character, + ;; we never exit the list. So we have to check whether we're looking at the close of a + ;; list, and if so, move just past it. + (cl-decf list-depth) + (1+ start)) + (when (looking-at-p (rx (or (syntax string-quote) + (syntax string-delimiter)))) + (forward-sexp 1) + (setf found-string-p t) + (point)) + ;; Don't go past the end of the line. + (apply #'min + (-non-nil + (list + (or (ignore-errors + ;; Scan to the past the delimiter of the next deeper list. + (scan-lists start 1 -1)) + (ignore-errors + ;; Scan to the end of the current list delimiter. + (1- (scan-lists start 1 1)))) + (line-end-position)))) + ;; If we can't find anything, return `limit'. I'm not sure if this is the correct + ;; thing to do, but it avoids an error (and possibly hanging Emacs) in the event of + ;; an undiscovered bug. Although, signaling an error might be better, because I + ;; have seen "redisplay" errors related to font-lock in the messages buffer before, + ;; which might mean that Emacs can handle that. I think the important thing is not + ;; to hang Emacs, to always either return nil or advance point to `limit'. + limit)))) + (when (< end start) + ;; Set search bound properly when `start' is greater than + ;; `end' (i.e. when `start' is moved past `limit', I think). + (setf end start)) + (when end + ;; End found: Try to fontify. + (unless (or in-string-p found-string-p found-comment-p) + ;; Neither in a string nor looking at nor in a comment. + (save-excursion + (or (when (re-search-forward (rx (syntax comment-start)) end t) + ;; Set `end' to any comment found before it. + (setf end (match-beginning 0))) + (when (re-search-forward (rx (or (syntax string-quote) + (syntax string-delimiter))) + end t) + ;; Set `end' to any string found before it. + (unless (nth 4 (syntax-ppss)) + ;; Not in a comment. + (setf end (match-beginning 0))))))) + (if (and (comment-p) (= 0 (depth-at))) + (setf prism-face nil) + (setf prism-face (face-at))) + (goto-char end) + (unless (> (point) start) + ;; Prevent end-of-buffer error in `font-lock-fontify-keywords-region'. + (cl-decf start)) + (set-match-data (list start end (current-buffer))) + (unless (> (point) starting-pos) + (prism-mode -1) + (error "prism: Infinite loop detected in `prism-match-whitespace' (buffer:%S point:%S). Please report this bug" + (current-buffer) (point))) + ;; Be sure to return non-nil! + t)))))) + +(cl-defun prism-remove-faces (&optional (beg (point-min))) + "Remove `prism' faces from buffer. +Note a minor bug at the moment: anonymous faces are also +removed." + (cl-macrolet ((without-prism-faces (faces) + `(cl-loop for face in ,faces + ;; FIXME: This removes anonymous faces. + unless (or (not (facep face)) + (string-prefix-p "prism-level-" (symbol-name face))) + collect face))) + (with-silent-modifications + (save-excursion + (goto-char beg) + (cl-loop for end = (or (next-single-property-change (point) 'face) (point-max)) + for faces = (get-text-property (point) 'face) + when faces + do (put-text-property (point) end 'face (without-prism-faces faces)) + for next-change = (next-single-property-change (point) 'face) + while (and next-change + (/= next-change (point-max))) + do (goto-char next-change)))))) + +;;;;; Colors + +(cl-defun prism-set-colors + (&key shuffle save local + (num prism-num-faces) (colors prism-colors) + (attribute prism-color-attribute) + (desaturations prism-desaturations) (lightens prism-lightens) + (comments-fn (lambda (color) + (--> color + (color-desaturate-name it 30) + (color-lighten-name it -10)))) + (strings-fn (lambda (color) + (--> color + (color-desaturate-name it 20) + (color-lighten-name it 10)))) + (parens-fn (lambda (color) + (prism-blend color (face-attribute 'default :background) 0.5)))) + "Set `prism' faces. Call after loading a new theme. +Call also when COLORS has been set to a list of faces and those +faces have been modified. + +NUM is the number of faces to set, i.e. the depth to make faces +for. + +When SAVE is non-nil, save attributes to `prism-' customization +options for future use by default. + +When LOCAL is t (interactively, with one universal prefix), remap +faces buffer-locally; when `reset' (interactively, with two +prefixes), clear local remapping and don't set any faces; when +nil (the default), set faces globally. + +COLORS is a list of one or more color name strings (like +\"green\" or \"#ff0000\") or face symbols (of which the +foreground color is used). + +DESATURATIONS and LIGHTENS are lists of integer percentages +applied to colors as depth increases; they need not be as long as +NUM, because they are extrapolated automatically. + +COMMENTS-FN, PARENS-FN, and STRINGS-FN are functions of one +argument, a color name or hex RGB string, which return the color +having been modified as desired for comments, parens, or strings, +respectively." + (declare (indent defun)) + (interactive) + (when (called-interactively-p 'any) + (setf local (pcase current-prefix-arg + ('(16) 'reset) + ('(4) t)))) + (when shuffle + (setf colors (prism-shuffle colors))) + ;; MAYBE: Extrapolate desaturations and lightens cleverly, instead + ;; of requiring the user to call `prism-extrapolate'. + (cl-labels ((faces (colors &optional suffix (fn #'identity)) + (setf suffix (if suffix + (concat "-" suffix) + "")) + (cl-loop for i from 0 below num + for face = (intern (format "prism-level-%d%s" i suffix)) + for color = (funcall fn (nth i colors)) + for description = (format "`prism' face%s #%d" suffix i) + do (set-face face attribute color description) + collect (cons i face))) + (set-face (face attribute color description) + (pcase local + ('nil + (when (internal-lisp-face-p face) + ;; Delete existing face, important if e.g. changing :foreground to :background. + (face-spec-set face nil 'customized-face)) + (custom-declare-face face '((t)) description :group 'prism-faces) + (set-face-attribute face nil attribute color)) + ('reset (reset-face face)) + (_ (face-remap-add-relative face (list attribute color))))) + (reset-face (face) + (--when-let (alist-get face face-remapping-alist) + (face-remap-remove-relative (cons (-last-item it) (car (butlast it))))))) + (let* ((colors (->> colors + (--map (pcase-exhaustive it + ((pred facep) (face-attribute it :foreground nil 'default)) + ((pred stringp) it) + ((pred functionp) (funcall it)) + (`(themed ,color) (prism-theme-color color)))) + (--remove (string-prefix-p "unspecified-" it)) + -cycle + (prism-modify-colors :num num + :desaturations desaturations + :lightens lightens + :colors) + ;; Use only two digits per component. HTML export of code (e.g. with Org + ;; Export, htmlize, etc.) doesn't work well with colors like "#01234567890a", + ;; even if Emacs can handle them internally. Maybe it's Web browsers that + ;; can't handle them. Anyway, we shouldn't use them if it breaks that. + (--map (--> (color-name-to-rgb it) + (-let (((r g b) it)) + (color-rgb-to-hex r g b 2))))))) + (cl-macrolet ((set-vars (&rest pairs) + `(progn + ,@(cl-loop for (var val) on pairs by #'cddr + collect `(pcase local + ('nil ;; Set global faces. + (set ',var ,val)) + ('reset ;; Clear local remappings. + ,val) + (_ ;; Remap locally. + (set (make-local-variable ',var) ,val))))))) + (set-vars prism-faces (faces colors) + prism-faces-strings (faces colors "strings" strings-fn) + prism-faces-comments (faces colors "comments" comments-fn) + prism-faces-parens (faces colors "parens" parens-fn))) + (when (and save (not local)) + ;; Save arguments for later saving as customized variables, + ;; including the unmodified (but shuffled) colors. + (setf prism-colors colors + prism-desaturations desaturations + prism-lightens lightens + prism-num-faces num + prism-comments-fn comments-fn + prism-strings-fn strings-fn + prism-parens-fn parens-fn) + (prism-save-colors))))) + +(defun prism-randomize-colors (&optional arg) + "Randomize `prism' colors using themed `font-lock' faces. +ARG may be a number (which limits the number of colors used), or +a universal prefix (to use all `font-lock' faces), or nil (to use +unique colors from `font-lock' faces)." + (interactive "P") + (cl-labels ((colorize ;; Return color NAME propertized with its foreground as its color. + (name) (propertize name 'face (list :foreground name))) + (faces ;; Return list of used colors with foreground color face applied. + () (->> (face-list) + (--select (and (string-prefix-p "prism-level" (symbol-name it)) + (string-match-p (rx digit eos) (symbol-name it)))) + nreverse (-map #'face-foreground) (-map #'colorize))) + (select-colors (colors threshold) + ;; Return shuffled list of COLORS ensuring that the + ;; distance between each one meets THRESHOLD. + (cl-loop with selected = (list (pop colors)) + while colors + do (setf colors (prism-shuffle colors)) + for index = (--find-index + (>= (color-distance (car selected) it) + threshold) + colors) + while index + do (progn + (push (nth index colors) selected) + (setf colors (-remove-at index colors))) + finally return selected)) + (background-contrast-p (color &optional (min-distance 32768)) + (>= (color-distance color (face-attribute 'default :background)) + min-distance)) + (option-customized-p + (option) (not (equal (pcase-exhaustive (get option 'standard-value) + (`((funcall (function ,fn))) (funcall fn))) + (symbol-value option))))) + (let* ((faces (--select (string-prefix-p "font-lock-" (symbol-name it)) + (face-list))) + (colors (->> faces + (--map (face-attribute it :foreground)) + (--remove (eq 'unspecified it)) + (-remove #'color-gray-p) + (-select #'background-contrast-p))) + (colors (pcase arg + ((pred integerp) (-take arg (prism-shuffle (-uniq colors)))) + ('(4) colors) + (_ (-uniq colors)))) + (colors (select-colors colors prism-color-distance)) + (colors (-rotate (random (length colors)) colors)) + (desaturations (if (option-customized-p 'prism-desaturations) + prism-desaturations + (prism-extrapolate 0 prism-num-faces (length colors) + (* c (+ 2 (length colors)))))) + (lightens (if (option-customized-p 'prism-lightens) + prism-lightens + (prism-extrapolate 0 prism-num-faces (length colors) + (* c (+ 2 (length colors))))))) + (prism-set-colors :colors colors + :desaturations desaturations + :lightens lightens + :comments-fn (if (option-customized-p 'prism-comments-fn) + prism-comments-fn + (lambda (color) + (--> color + ;; The default function desaturates by 30%, but 40% + ;; seems to help a bit when using random colors. + (color-desaturate-name it 40) + (color-lighten-name it -10))))) + (message "Randomized%s colors: %s\nFaces: %s" + (pcase arg + ('(4) "") + (_ ", unique")) + (string-join (-map #'colorize colors) " ") + (string-join (faces) " "))))) + +(defun prism-save-colors () + "Save current `prism' colors. +Function `prism-set-colors' does not save its argument values +permanently. This command saves them using the customization +system so that `prism-set-colors' can then be called without +arguments to set the same faces." + ;; FIXME: Make this interactive. + (cl-letf (((symbol-function 'custom-save-all) + (symbol-function 'ignore))) + ;; Avoid saving the file for each variable, which is very slow. + ;; Save it once at the end. + (dolist (var (list 'prism-desaturations 'prism-lightens 'prism-num-faces + 'prism-comments-fn 'prism-strings-fn)) + (customize-save-variable var (symbol-value var)))) + (customize-save-variable 'prism-colors prism-colors)) + +(cl-defun prism-modify-colors (&key num colors desaturations lightens &allow-other-keys) + "Return list of NUM colors modified according to DESATURATIONS and LIGHTENS." + (cl-flet ((modify-color (color desaturate lighten) + (--> color + (if (> desaturate 0) + (color-desaturate-name it desaturate) + it) + (if (> lighten 0) + (color-lighten-name it lighten) + it) + ;; FIXME: It seems that these two functions called in sequence + ;; always modify the color, e.g. #ff2afc becomes #fe29fb. + (color-name-to-rgb it) + (-let (((r g b) it)) + (color-rgb-to-hex r g b 2))))) + (when (< (length desaturations) num) + (setf desaturations (prism-expand-list num desaturations))) + (when (< (length lightens) num) + (setf lightens (prism-expand-list num lightens))) + (cl-loop for i from 0 below num + for desaturate = (nth i desaturations) + for lighten = (nth i lightens) + collect (modify-color (nth i colors) desaturate lighten)))) + +(defun prism-blend (a b alpha) + "Return color A blended with color B by amount ALPHA." + (cl-flet ((blend (a b alpha) + (+ (* alpha a) (* b (- 1 alpha))))) + (-let* (((ar ag ab) (color-name-to-rgb a)) + ((br bg bb) (color-name-to-rgb b))) + (color-rgb-to-hex (blend ar br alpha) + (blend ag bg alpha) + (blend ab bb alpha))))) + +(defun prism-shuffle (seq) + "Destructively shuffle SEQ. +Copied from `elfeed-shuffle'." + (let ((n (length seq))) + (prog1 seq ; don't use dotimes result (bug#16206) + (dotimes (i n) + (cl-rotatef (elt seq i) (elt seq (+ i (cl-random (- n i))))))))) + +(defun prism-expand-list (new-length list) + "Return LIST expanded to NEW-LENGTH. +Each element of LIST is repeated an equal number of times, except +that the last element may be repeated an extra time when +necessary." + (let* ((length (length list)) + (_longer-p (or (> new-length length) + (user-error "NEW-LENGTH must be longer than LIST"))) + (repeat-n (/ new-length (if (zerop (mod new-length length)) + length + (1- length)))) + (final-element-p (not (zerop (mod new-length length)))) + (new-list (->> list + (--map (-repeat repeat-n it)) + (-flatten)))) + (if final-element-p + (-snoc new-list (-last-item list)) + new-list))) + +(defun prism-customize-set (option value) + "Set OPTION to VALUE, and call `prism-set-colors' when possible." + (set-default option value) + (when (--all? (and (boundp it) (symbol-value it)) + '(prism-num-faces prism-color-attribute prism-desaturations + prism-lightens prism-comments-fn prism-strings-fn prism-colors)) + ;; We can't call `prism-set-colors' until *all* relevant options + ;; have been set. + (prism-set-colors))) + +(declare-function doom-color "ext:doom-themes" t) + +(defun prism-theme-color (color) + "Return COLOR (a string) from current `doom' or `solarized' theme. +If no `doom' or `solarized' theme is active, return COLOR. +Assumes the first `doom' or `solarized' theme found in +`custom-enabled-themes' is the active one." + (if (string-empty-p color) + color + (if-let* ((active-theme (--first (or (string-match (rx bos "doom-" (group (1+ anything))) + (symbol-name it)) + (string-match (rx bos "solarized-" (group (1+ anything))) + (symbol-name it))) + custom-enabled-themes)) + (theme-name (symbol-name active-theme))) + (pcase theme-name + ((rx bos "solarized-") + (let ((variant (intern (string-trim theme-name (rx "solarized-")))) + (color (intern color))) + ;; Yes, `eval' is evil, but for some reason I can't figure out, + ;; it's the only way this works here. In a test function, + ;; `symbol-value' worked fine, but not here. Go figure. + (eval `(solarized-with-color-variables ',variant + ,color)))) + ((rx bos "doom-") + (or (doom-color (intern color)) + color))) + color))) + +;;;; Customization + +;; These are at the bottom because the setters call `prism-set-faces', +;; which is defined above. + +(defgroup prism nil + "Disperse lisp forms into a spectrum of colors according to depth." + :group 'font-lock + :link '(url-link "https://github.com/alphapapa/prism.el")) + +(defcustom prism-num-faces 16 + "Number of `prism' faces." + :type 'integer + :set #'prism-customize-set) + +(defcustom prism-color-attribute :foreground + "Face attribute set in `prism' faces." + :type '(choice (const :tag "Foreground" :foreground) + (const :tag "Background" :background)) + :set #'prism-customize-set) + +(defcustom prism-desaturations '(40 50 60) + "Default desaturation percentages applied to colors as depth increases. +This need not be as long as the number of faces used, because +it's extrapolated to the length of `prism-faces'." + :type '(repeat number) + :set #'prism-customize-set) + +(defcustom prism-lightens '(0 5 10) + "Default lightening percentages applied to colors as depth increases. +This need not be as long as the number of faces used, because +it's extrapolated to the length of `prism-faces'." + :type '(repeat number) + :set #'prism-customize-set) + +(defcustom prism-comments t + "Whether to colorize comments. +Note that comments at depth 0 are not colorized, which preserves +the appearance of e.g. commented Lisp headings." + :type 'boolean) + +(defcustom prism-comments-fn + (lambda (color) + (prism-blend color (face-attribute 'font-lock-comment-face :foreground) 0.25)) + "Function which adjusts colors for comments. +Receives one argument, a color name or hex RGB string." + :type 'function + :set #'prism-customize-set) + +(defcustom prism-strings t + "Whether to fontify strings." + :type 'boolean) + +(defcustom prism-strings-fn + (lambda (color) + (prism-blend color "white" 0.5)) + "Function which adjusts colors for strings. +Receives one argument, a color name or hex RGB string." + :type 'function + :set #'prism-customize-set) + +(defcustom prism-parens nil + "Whether to colorize parens separately. +When disabled, parens are colorized with the same face as the +other elements at their depth. When enabled, parens may be +colorized distinctly, e.g. to make them fade away or stand out. +See the PARENS-FN argument to the `prism-set-colors' function." + :type 'boolean + :set #'prism-customize-set) + +(defcustom prism-colors + (list 'font-lock-type-face 'font-lock-function-name-face + 'font-lock-keyword-face 'font-lock-doc-face) + "List of colors used by default." + :type '(repeat (choice (face :tag "Face (using its foreground color)") + color + (list :tag "Doom/Solarized theme color (requires active theme)" + (const themed) + (string :tag "Color name")) + (function :tag "Function which returns a color"))) + :set #'prism-customize-set) + +(defcustom prism-color-distance 32768 + "Minimum distance between randomized colors. +See `color-distance'." + :type 'integer) + +(defgroup prism-faces nil + "Faces for `prism'. Set automatically with `prism-set-colors'. Do not set manually." + ;; Define a group for the faces to keep them out of the main + ;; customization group, otherwise users might customize them there + ;; and get confused. Define this group after all other `defcustom's + ;; so the "current group" isn't changed before they're all defined. + :group 'prism) + +(defcustom prism-whitespace-mode-indents + (list (cons 'python-mode 'python-indent-offset) + (cons 'haskell-mode 'haskell-indentation-left-offset) + (cons t 4)) + "Alist mapping major modes to indentation offsets for `prism-whitespace-mode'. +Each key should be a major mode function symbol, and the value +either a variable whose value to use or an integer number of +spaces. The last cell is the default, and its key should be t." + :type '(alist :key-type (choice (const :tag "Default" t) + (symbol :tag "Major mode")) + :value-type (choice (variable :tag "Value of variable") + (integer :tag "Number of spaces")))) + +;;;; Footer + +(provide 'prism) + +;;; prism.el ends here diff --git a/init.el b/init.el @@ -243,8 +243,13 @@ (("RET" . corfu-insert-with-return)) corfu) -(add-hook 'lisp-mode-hook #'paredit-mode) -(add-hook 'emacs-lisp-mode-hook #'paredit-mode) +(defun lh/lisp-mode-hook () + (paredit-mode 1) + (prism-mode 1) + (highlight-function-calls-mode 1)) + +(add-hook 'lisp-mode-hook 'lh/lisp-mode-hook) +(add-hook 'emacs-lisp-mode-hook 'lh/lisp-mode-hook) ;; Aggressive Intent mode causes 100% CPU for me whenever the sly repl prints warnings (add-hook 'sly-mrepl-mode-hook (lambda () @@ -390,9 +395,10 @@ ("melpa" . "https://melpa.org/packages/"))) '(package-pinned-packages '((sly . "melpa"))) '(package-selected-packages - '(modus-themes imenu-list diff-hl embark-consult embark all-the-icons-completion all-the-icons-ibuffer all-the-icons-dired sly-named-readtables sly-macrostep denote-refs denote-menu denote ox-epub ob-powershell powershell web-mode lexic editorconfig elfeed-tube-mpv elfeed-tube cider restclient-jq graphviz-dot-mode consult-eglot jq-mode ob-restclient restclient vterm deadgrep helpful pdf-tools paredit-menu paredit corfu sly eglot aggressive-indent project nov nhexl-mode elfeed magit yaml-mode json-mode lua-mode go-mode geiser-guile geiser org-contrib org ace-window expand-region consult marginalia uuidgen request diminish which-key)) + '(highlight-function-calls prism modus-themes imenu-list diff-hl embark-consult embark all-the-icons-completion all-the-icons-ibuffer all-the-icons-dired sly-named-readtables sly-macrostep denote-refs denote-menu denote ox-epub ob-powershell powershell web-mode lexic editorconfig elfeed-tube-mpv elfeed-tube cider restclient-jq graphviz-dot-mode consult-eglot jq-mode ob-restclient restclient vterm deadgrep helpful pdf-tools paredit-menu paredit corfu sly eglot aggressive-indent project nov nhexl-mode elfeed magit yaml-mode json-mode lua-mode go-mode geiser-guile geiser org-contrib org ace-window expand-region consult marginalia uuidgen request diminish which-key)) '(pcomplete-ignore-case t t) '(pixel-scroll-precision-mode t) + '(prism-parens t) '(read-buffer-completion-ignore-case t) '(read-file-name-completion-ignore-case t) '(reb-re-syntax 'string) @@ -417,6 +423,27 @@ ;; This is the place where I override all customize stuff +(prism-set-colors + :desaturations '(0) + :lightens '(0) + :colors (modus-themes-with-colors + (list fg-main + magenta + cyan-cooler + magenta-cooler + blue + magenta-warmer + cyan-warmer + red-cooler + green + fg-main + cyan + yellow + blue-warmer + red-warmer + green-cooler + yellow-faint))) + (let ((feeds (expand-file-name "feeds.el" user-emacs-directory))) (when (file-exists-p feeds) (load feeds)))