├── .github └── workflows │ └── ci.yml ├── README.md ├── build.ninja ├── color-identifiers-mode.el └── tests.el /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest, windows-latest] 12 | emacs-version: 13 | - 27.2 14 | # snapshot has additional checks don't work on earlier versions, 15 | # see TODO in tests.el 16 | - snapshot 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: jcs090218/setup-emacs@master 22 | with: 23 | version: ${{ matrix.emacs-version }} 24 | 25 | - uses: ashutoshvarma/setup-ninja@master 26 | 27 | - name: Run tests 28 | run: ninja tests 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Color Identifiers Mode 2 | [![MELPA](http://melpa.org/packages/color-identifiers-mode-badge.svg)](http://melpa.org/#/color-identifiers-mode) 3 | 4 | Color Identifiers is a minor mode for Emacs that highlights each source code identifier uniquely based on its name. It is inspired by a [post by Evan Brooks](https://medium.com/p/3a6db2743a1e/). 5 | 6 | Currently it supports Scala (scala-mode2), JavaScript (js-mode and js2-mode), Ruby, Python, Emacs Lisp, Clojure, C, C++, Rust, Java, and Go. You can add support for your favorite mode by modifying `color-identifiers:modes-alist` and optionally calling `color-identifiers:set-declaration-scan-fn`. 7 | 8 | [Check out the demo.](http://youtu.be/g4qsiAo2aac) 9 | 10 | ![Screenshot of Color Identifiers Mode on Scala](https://raw.github.com/ankurdave/color-identifiers-mode/gh-pages/demo-static.png) 11 | 12 | It picks colors adaptively to fit the theme: 13 | 14 | ![Different Themes](https://raw.github.com/ankurdave/color-identifiers-mode/gh-pages/themes.png) 15 | 16 | Use `M-x color-identifiers:regenerate-colors` after a theme change. 17 | 18 | ## Installation 19 | Color Identifiers is in [MELPA](https://github.com/milkypostman/melpa/pull/1416). First [set up MELPA](https://github.com/milkypostman/melpa#usage): 20 | 21 | ```lisp 22 | (package-initialize) 23 | (add-to-list 'package-archives 24 | '("melpa" . "https://melpa.org/packages/") t) 25 | (package-refresh-contents) 26 | ``` 27 | 28 | Then install it: 29 | 30 | ```lisp 31 | (package-install 'color-identifiers-mode) 32 | ``` 33 | 34 | Finally, visit a supported file and type `M-x color-identifiers-mode`. 35 | 36 | If you like it, enable it for all supported files by adding the following to your init file: 37 | 38 | ```lisp 39 | (add-hook 'after-init-hook 'global-color-identifiers-mode) 40 | ``` 41 | 42 | ## Configuration 43 | 44 | * Recoloring delay: the time before recoloring newly appeared identifiers is `2` seconds by default. To change it e.g. to `1` second add to your config `(setq color-identifiers:recoloring-delay 1)` 45 | * Additional face-properties *(such as italic or bold)* can be added to the identifiers by modifying `color-identifiers:extra-face-attributes`. E.g. to make identifiers look bold use `(setq color-identifiers:extra-face-attributes '(:weight bold))`. But make sure not to change `:foreground` because it is the color of identifiers. 46 | * To make the variables stand out, you can turn off highlighting for all other keywords in supported modes using a code like: 47 | ```lisp 48 | (defun myfunc-color-identifiers-mode-hook () 49 | (let ((faces '(font-lock-comment-face font-lock-comment-delimiter-face font-lock-constant-face font-lock-type-face font-lock-function-name-face font-lock-variable-name-face font-lock-keyword-face font-lock-string-face font-lock-builtin-face font-lock-preprocessor-face font-lock-warning-face font-lock-doc-face font-lock-negation-char-face font-lock-regexp-grouping-construct font-lock-regexp-grouping-backslash))) 50 | (dolist (face faces) 51 | (face-remap-add-relative face '(:inherit default)))) 52 | (face-remap-add-relative 'font-lock-keyword-face '((:weight bold))) 53 | (face-remap-add-relative 'font-lock-comment-face '((:slant italic))) 54 | (face-remap-add-relative 'font-lock-builtin-face '((:weight bold))) 55 | (face-remap-add-relative 'font-lock-preprocessor-face '((:weight bold))) 56 | (face-remap-add-relative 'font-lock-function-name-face '((:slant italic))) 57 | (face-remap-add-relative 'font-lock-string-face '((:slant italic))) 58 | (face-remap-add-relative 'font-lock-constant-face '((:weight bold)))) 59 | (add-hook 'color-identifiers-mode-hook 'myfunc-color-identifiers-mode-hook) 60 | ``` 61 | 62 | ![Other Keywords Dimmed](https://raw.github.com/ankurdave/color-identifiers-mode/gh-pages/dim-other-keywords.png) 63 | 64 | ## Contributing 65 | 66 | After having made changes to `color-identifiers-mode.el` you can test for regressions by running `ninja tests`. It checks lack of byte-compilation warnings and color-highlighting in various modes. 67 | 68 | Improvements to the tests or the core mode are always welcome! 69 | -------------------------------------------------------------------------------- /build.ninja: -------------------------------------------------------------------------------- 1 | rule batch_emacs 2 | command = emacs -batch $args 3 | pool = console 4 | 5 | rule compile_file 6 | command = emacs -batch --eval "(setq byte-compile-error-on-warn t)" $args -f batch-byte-compile $in 7 | 8 | build dash.el: batch_emacs 9 | # In the code below we remove dash.el manually, because when it's "dirty", 10 | # url-copy-file won't overwrite it (it may ignore it but that's different). 11 | args = --eval "(progn$ 12 | (require 'url)$ 13 | (delete-file \"dash.el\")$ 14 | (url-copy-file \"https://raw.githubusercontent.com/magnars/dash.el/master/dash.el\" \"dash.el\"))" 15 | 16 | build color-identifiers-mode.elc: compile_file color-identifiers-mode.el | dash.el 17 | args = -l dash.el 18 | 19 | build tests.elc: compile_file tests.el | dash.el color-identifiers-mode.elc 20 | args = -l dash.el -l color-identifiers-mode.elc 21 | 22 | build tests: batch_emacs | dash.el color-identifiers-mode.elc tests.elc 23 | args = -l dash.el -l color-identifiers-mode.elc -l tests.elc -f ert-run-tests-batch-and-exit 24 | -------------------------------------------------------------------------------- /color-identifiers-mode.el: -------------------------------------------------------------------------------- 1 | ;;; color-identifiers-mode.el --- Color identifiers based on their names -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2014 Ankur Dave 4 | 5 | ;; Author: Ankur Dave 6 | ;; Url: https://github.com/ankurdave/color-identifiers-mode 7 | ;; Created: 24 Jan 2014 8 | ;; Version: 1.1 9 | ;; Keywords: faces, languages 10 | ;; Package-Requires: ((dash "2.5.0") (emacs "24.4")) 11 | 12 | ;; This file is not a part of GNU Emacs. 13 | 14 | ;; This program is free software: you can redistribute it and/or modify 15 | ;; it under the terms of the GNU General Public License as published by 16 | ;; the Free Software Foundation, either version 3 of the License, or 17 | ;; (at your option) any later version. 18 | 19 | ;; This program is distributed in the hope that it will be useful, 20 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | ;; GNU General Public License for more details. 23 | 24 | ;; You should have received a copy of the GNU General Public License 25 | ;; along with this program. If not, see . 26 | 27 | ;;; Commentary: 28 | 29 | ;; Color Identifiers is a minor mode for Emacs that highlights each source code 30 | ;; identifier uniquely based on its name. It is inspired by a post by Evan 31 | ;; Brooks: https://medium.com/p/3a6db2743a1e/ 32 | 33 | ;; Check out the project page, which has screenshots, a demo, and usage 34 | ;; instructions: https://github.com/ankurdave/color-identifiers-mode 35 | 36 | ;;; Code: 37 | 38 | (require 'nadvice) 39 | (require 'color) 40 | (require 'dash) 41 | (require 'cl-lib) 42 | (require 'rx) 43 | (require 'subr-x) 44 | 45 | (defgroup color-identifiers nil "Color identifiers based on their names." 46 | :group 'faces) 47 | 48 | (defvar color-identifiers:timer nil 49 | "Timer for running `color-identifiers:refresh'.") 50 | 51 | (defvar color-identifiers:recoloring-delay 2 52 | "The delay before running `color-identifiers:refresh'.") 53 | 54 | (defun color-identifiers:enable-timer () 55 | (if color-identifiers:timer 56 | ;; Someone set the timer. Activate in case we cancelled it. 57 | (unless (memq color-identifiers:timer timer-idle-list) 58 | (timer-activate-when-idle color-identifiers:timer)) 59 | (setq color-identifiers:timer 60 | (run-with-idle-timer color-identifiers:recoloring-delay t 'color-identifiers:refresh))) 61 | ) 62 | 63 | (defvar-local color-identifiers:colorize-behavior nil 64 | "For internal use. Stores the element of 65 | `color-identifiers:modes-alist' that is relevant to the current 66 | major mode") 67 | 68 | (defun color-identifiers:regen-on-theme-change(_) 69 | "Regenerate colors for color-identifiers-mode on theme change." 70 | (color-identifiers:regenerate-colors)) 71 | 72 | ;;; USER-VISIBLE VARIABLES AND FUNCTIONS ======================================= 73 | 74 | (defcustom color-identifiers-coloring-method 'sequential 75 | "How to assign colors: sequentially or using the hash of the identifier. 76 | Sequential color assignment (the default) reduces collisions 77 | between adjacent identifiers. Hash-based color assignment ensures 78 | that a particular identifier is always assigned the same color 79 | across buffers." 80 | :type '(choice 81 | (const :tag "Sequential" sequential) 82 | (const :tag "Hash-based" hash))) 83 | 84 | 85 | (defcustom color-identifiers-avoid-faces nil 86 | "Which color faces to avoid: A list of faces whose foreground 87 | color should be avoided when generating colors, this can be warning colors, 88 | error colors etc." 89 | :type '(repeat face)) 90 | 91 | (defcustom color-identifiers:extra-face-attributes nil 92 | "Extra face attributes to apply to identifiers. Can be used to make 93 | identifiers bold or italic, but avoid changing `:foreground' 94 | because it is the color determined by the mode." 95 | :type 'plist 96 | :group 'color-identifiers) 97 | 98 | (defvar color-identifiers:modes-alist nil 99 | "Alist of major modes and the ways to distinguish identifiers in those modes. 100 | The value of each cons cell provides four constraints for finding 101 | identifiers. A word must match all four constraints to be 102 | colored as an identifier. The cons cell has the form (MAJOR-MODE 103 | IDENTIFIER-CONTEXT-RE IDENTIFIER-RE IDENTIFIER-FACES 104 | IDENTIFIER-EXCLUSION-RE). 105 | 106 | MAJOR-MODE is the name of the mode in which this rule should be used. 107 | IDENTIFIER-CONTEXT-RE is a regexp matching the text that must precede an 108 | identifier. 109 | IDENTIFIER-RE is a regexp whose first capture group matches identifiers. 110 | IDENTIFIER-FACES is a list of faces with which the major mode decorates 111 | identifiers or a function returning such a list. If the list includes nil, 112 | unfontified words will be considered. 113 | IDENTIFIER-EXCLUSION-RE is a regexp that must not match identifiers, 114 | or nil. 115 | 116 | If a scan function is registered for a mode, candidate 117 | identifiers will be further restricted to those returned by the 118 | scan function.") 119 | 120 | (defvar color-identifiers:num-colors 10 121 | "The number of different colors to generate.") 122 | 123 | (defvar color-identifiers:color-luminance nil 124 | "HSL luminance of identifier colors. If nil, calculated from the luminance 125 | of the default face.") 126 | 127 | (defvar color-identifiers:min-color-saturation 0.0 128 | "The minimum saturation that identifier colors will be generated with.") 129 | 130 | (defvar color-identifiers:max-color-saturation 1.0 131 | "The maximum saturation that identifier colors will be generated with.") 132 | 133 | (defvar color-identifiers:mode-to-scan-fn-alist nil 134 | "Alist from major modes to their declaration scan functions, for internal use. 135 | If no scan function is registered for a particular mode, all 136 | candidates matching the constraints in 137 | `color-identifiers:modes-alist' will be colored. 138 | 139 | Modify this variable using 140 | `color-identifiers:set-declaration-scan-fn'.") 141 | 142 | (defvar color-identifiers-mode-hook nil 143 | "List of functions to run every time the mode enabled") 144 | 145 | (defvar color-identifiers:re-not-inside-class-access 146 | (rx (or (not (any ".")) line-start) 147 | (zero-or-more space)) 148 | "This regexp matches anything except inside a class instance 149 | access, like foo.bar" ) 150 | 151 | (defun color-identifiers:set-declaration-scan-fn (mode scan-fn) 152 | "Register SCAN-FN as the declaration scanner for MODE. 153 | SCAN-FN must scan the entire current buffer and return the 154 | identifiers to highlight as a list of strings. Only identifiers 155 | produced by SCAN-FN that also match all constraints in 156 | `color-identifiers:modes-alist' will be colored. 157 | 158 | See `color-identifiers:elisp-get-declarations' for an example 159 | SCAN-FN." 160 | (let ((entry (assoc mode color-identifiers:mode-to-scan-fn-alist))) 161 | (if entry 162 | (setcdr entry scan-fn) 163 | (add-to-list 'color-identifiers:mode-to-scan-fn-alist 164 | (cons mode scan-fn))))) 165 | 166 | (defsubst color-identifiers:curr-identifier-faces () 167 | (if (functionp (nth 3 color-identifiers:colorize-behavior)) 168 | (funcall (nth 3 color-identifiers:colorize-behavior)) 169 | (nth 3 color-identifiers:colorize-behavior))) 170 | 171 | ;;; MAJOR MODE SUPPORT ========================================================= 172 | 173 | (defun color-identifiers:get-declarations () 174 | "Extract a list of identifiers declared in the current buffer." 175 | (let ((result (make-hash-table :test 'equal)) 176 | (identifier-faces (color-identifiers:curr-identifier-faces))) 177 | ;; Entities that major mode highlighted as variables 178 | (save-excursion 179 | (let ((next-change (next-property-change (point-min)))) 180 | (while next-change 181 | (goto-char next-change) 182 | (let ((face-at-point (get-text-property (point) 'face))) 183 | (when (or (and face-at-point (memq face-at-point identifier-faces)) 184 | ;; If we fontified X in the past, keep X in the list for 185 | ;; consistency. Otherwise `scan-identifiers' will stop 186 | ;; colorizing new Xes while older ones remain colorized. 187 | (get-text-property (point) 'color-identifiers:fontified)) 188 | (puthash (substring-no-properties (symbol-name (symbol-at-point))) t result))) 189 | (setq next-change (next-property-change (point)))))) 190 | (hash-table-keys result))) 191 | 192 | (dolist (maj-mode '(c-mode c++-mode java-mode rust-mode rust-ts-mode rustic-mode meson-mode typescript-mode cuda-mode tsx-ts-mode typescript-ts-mode)) 193 | (add-to-list 194 | 'color-identifiers:modes-alist 195 | `(,maj-mode . ("" 196 | "\\_<\\([a-zA-Z_$]\\(?:\\s_\\|\\sw\\)*\\)" 197 | (nil font-lock-variable-name-face tree-sitter-hl-face:variable))))) 198 | 199 | ;; Scala 200 | (add-to-list 201 | 'color-identifiers:modes-alist 202 | `(scala-mode . (,color-identifiers:re-not-inside-class-access 203 | "\\_<\\([[:lower:]]\\([_]??[[:lower:][:upper:]\\$0-9]+\\)*\\(_+[#:<=>@!%&*+/?\\\\^|~-]+\\|_\\)?\\)" 204 | (nil scala-font-lock:var-face font-lock-variable-name-face tree-sitter-hl-face:variable)))) 205 | 206 | ;;; JavaScript 207 | (add-to-list 208 | 'color-identifiers:modes-alist 209 | `(js-mode . (,color-identifiers:re-not-inside-class-access 210 | "\\_<\\([a-zA-Z_$]\\(?:\\s_\\|\\sw\\)*\\)" 211 | (nil font-lock-variable-name-face)))) 212 | 213 | (add-to-list 214 | 'color-identifiers:modes-alist 215 | `(js2-mode . (,color-identifiers:re-not-inside-class-access 216 | "\\_<\\([a-zA-Z_$]\\(?:\\s_\\|\\sw\\)*\\)" 217 | (nil font-lock-variable-name-face js2-function-param)))) 218 | 219 | (add-to-list 220 | 'color-identifiers:modes-alist 221 | `(js3-mode . (,color-identifiers:re-not-inside-class-access 222 | "\\_<\\([a-zA-Z_$]\\(?:\\s_\\|\\sw\\)*\\)" 223 | (nil font-lock-variable-name-face js3-function-param-face)))) 224 | 225 | (add-to-list 226 | 'color-identifiers:modes-alist 227 | `(js-jsx-mode . (,color-identifiers:re-not-inside-class-access 228 | "\\_<\\([a-zA-Z_$]\\(?:\\s_\\|\\sw\\)*\\)" 229 | (nil font-lock-variable-name-face js2-function-param)))) 230 | 231 | (add-to-list 232 | 'color-identifiers:modes-alist 233 | `(js2-jsx-mode . (,color-identifiers:re-not-inside-class-access 234 | "\\_<\\([a-zA-Z_$]\\(?:\\s_\\|\\sw\\)*\\)" 235 | (nil font-lock-variable-name-face js2-function-param)))) 236 | 237 | ;; CoffeeScript 238 | ;; May need to add the @ to the symbol syntax 239 | ;; (add-hook 'coffee-mode-hook (lambda () (modify-syntax-entry ?\@ "_"))) in .emacs 240 | (add-to-list 241 | 'color-identifiers:modes-alist 242 | `(coffee-mode . (,color-identifiers:re-not-inside-class-access "\\_<\\([a-zA-Z_$@]\\(?:\\s_\\|\\sw\\)*\\)" (nil font-lock-variable-name-face)))) 243 | 244 | ;; Sgml mode and the like 245 | (dolist (maj-mode '(sgml-mode html-mode jinja2-mode)) 246 | (add-to-list 247 | 'color-identifiers:modes-alist 248 | `(,maj-mode . (" binding-form init-expr 431 | ((or `(let . ,rest) 432 | `(loop . ,rest)) 433 | (append (when (sequencep (car rest)) 434 | (let* ((bindings (append (car rest) nil)) 435 | (even-indices 436 | (-filter 'cl-evenp (number-sequence 0 (1- (length bindings))))) 437 | (binding-forms (-select-by-indices even-indices bindings))) 438 | (color-identifiers:clojure-extract-params binding-forms))) 439 | (color-identifiers:clojure-declarations-in-sexp rest))) 440 | ;; (fn name? [binding-form*] exprs*) 441 | ;; (fn name? ([binding-form*] exprs*)+) 442 | (`(fn . ,rest) 443 | (let* ((binding-forms+exprs (if (symbolp (car rest)) (cdr rest) rest)) 444 | (binding-forms (if (vectorp (car binding-forms+exprs)) 445 | (elt binding-forms+exprs 0) 446 | (mapcar 'car binding-forms+exprs))) 447 | (params (color-identifiers:clojure-extract-params binding-forms))) 448 | (append params (color-identifiers:clojure-declarations-in-sexp rest)))) 449 | ;; (defn name doc-string? attr-map? [binding-form*] body) 450 | ;; (defn name doc-string? attr-map? ([binding-form*] body)+) 451 | ((or `(defn ,_ . ,rest) 452 | `(defn- ,_ . ,rest) 453 | `(defmacro ,_ . ,rest)) 454 | (let ((params (-mapcat (lambda (params+body) 455 | (when (color-identifiers:clojure-contains-binding-forms-p params+body) 456 | (color-identifiers:clojure-extract-params params+body))) 457 | rest))) 458 | (append params (color-identifiers:clojure-declarations-in-sexp rest)))) 459 | (`nil nil) 460 | ((pred consp) 461 | (let ((cons sexp) 462 | (result nil)) 463 | (while (consp cons) 464 | (let ((ids (color-identifiers:clojure-declarations-in-sexp (car cons)))) 465 | (when ids 466 | (setq result (append ids result)))) 467 | (setq cons (cdr cons))) 468 | (when cons 469 | ;; `cons' is non-nil but also non-cons 470 | (let ((ids (color-identifiers:clojure-declarations-in-sexp cons))) 471 | (when ids 472 | (setq result (append ids result))))) 473 | result)) 474 | ((pred arrayp) 475 | (apply 'append (mapcar 'color-identifiers:clojure-declarations-in-sexp sexp))))) 476 | 477 | (defun color-identifiers:clojure-get-declarations () 478 | "Extract a list of identifiers declared in the current buffer. 479 | For Clojure support within color-identifiers-mode. 480 | 481 | TODO: Fails on top-level sexps containing Clojure syntax that is 482 | incompatible with Emacs Lisp syntax, such as reader macros (#)." 483 | (let ((result nil)) 484 | (save-excursion 485 | (goto-char (point-min)) 486 | (condition-case nil 487 | (while t 488 | (condition-case nil 489 | (let* ((sexp (read (current-buffer))) 490 | (ids (color-identifiers:clojure-declarations-in-sexp sexp)) 491 | (strs (-filter (lambda (id) (if (member id '("&" ":as")) nil id)) 492 | (mapcar (lambda (id) 493 | (when (symbolp id) (symbol-name id))) 494 | ids)))) 495 | (setq result (append strs result))) 496 | (invalid-read-syntax nil) 497 | (wrong-type-argument nil))) 498 | (end-of-file nil))) 499 | (delete-dups result) 500 | result)) 501 | 502 | (dolist (mode '(clojure-mode 503 | clojure-ts-mode 504 | clojurescript-mode)) 505 | (color-identifiers:set-declaration-scan-fn 506 | mode 'color-identifiers:clojure-get-declarations) 507 | (add-to-list 508 | 'color-identifiers:modes-alist 509 | `(,mode . ("" 510 | "\\_<\\(\\(?:\\s_\\|\\sw\\)+\\)" 511 | (nil))))) 512 | 513 | (dolist (maj-mode '(tuareg-mode sml-mode)) 514 | (add-to-list 515 | 'color-identifiers:modes-alist 516 | `(,maj-mode . ("" 517 | "\\_<\\([a-zA-Z_$]\\(?:\\s_\\|\\sw\\|'\\)*\\)" 518 | (nil font-lock-variable-name-face))))) 519 | 520 | ;; R support in ess-mode 521 | (defun color-identifiers:remove-string-or-comment (str) 522 | "Remove string or comment in str, based on font lock faces" 523 | (let ((remove (memq (get-text-property 0 'face str) 524 | '(font-lock-string-face font-lock-comment-face))) 525 | (pos 0) 526 | (nextpos) 527 | (result "")) 528 | (while (setq nextpos (next-single-property-change pos 'face str)) 529 | (unless remove 530 | (setq result (concat result (substring-no-properties str pos nextpos)))) 531 | (setq pos nextpos) 532 | (setq remove (memq (get-text-property pos 'face str) 533 | '(font-lock-string-face font-lock-comment-face)))) 534 | (unless remove 535 | (setq result (concat result (substring-no-properties str pos nextpos)))) 536 | result)) 537 | 538 | (defun color-identifiers:r-get-args (lend) 539 | "Extract a list of function arg names. LEND is the point at 540 | the left parenthesis, after `function' keyword." 541 | (let* ((rend (save-excursion 542 | (goto-char lend) 543 | (forward-sexp) 544 | (point))) 545 | (str (color-identifiers:remove-string-or-comment 546 | (buffer-substring (1+ lend) (1- rend))))) 547 | (mapcar (lambda (s) (replace-regexp-in-string "\\s *=.*" "" s)) 548 | (split-string str "," t " ")))) 549 | 550 | (defun color-identifiers:r-get-declarations () 551 | "Extract a list of identifiers declared in the current buffer. 552 | For Emacs Lisp support within color-identifiers-mode." 553 | (let ((result nil)) 554 | (save-excursion 555 | (goto-char (point-min)) 556 | (while (re-search-forward "\\(\\(?:\\w\\|\\s_\\)*\\)\\s *< (cdr x) (cdr y))) min-dists))) 628 | (funcall choose-candidate (car best)))) 629 | (setq color-identifiers:colors 630 | (vconcat (-map (lambda (lab) 631 | (let* ((srgb (apply 'color-lab-to-srgb lab)) 632 | (rgb (mapcar 'color-clamp srgb))) 633 | (apply 'color-rgb-to-hex rgb))) 634 | chosens)))))) 635 | 636 | (defvar-local color-identifiers:color-index-for-identifier nil 637 | "Hashtable of identifier-index pairs for internal use. 638 | The index refers to `color-identifiers:colors'. Only used when 639 | `color-identifiers-coloring-method' is `sequential'.") 640 | 641 | (defvar-local color-identifiers:identifiers nil 642 | "Set of identifiers in the current buffer. 643 | Only used when `color-identifiers-coloring-method' is `hash' and 644 | a declaration scan function is registered for the current major 645 | mode. This variable memoizes the result of the declaration scan function.") 646 | 647 | ;;;###autoload 648 | (define-minor-mode color-identifiers-mode 649 | "Color the identifiers in the current buffer based on their names." 650 | :init-value nil 651 | :lighter " ColorIds" 652 | (if color-identifiers-mode 653 | (progn 654 | (setq color-identifiers:colorize-behavior 655 | (assoc major-mode color-identifiers:modes-alist)) 656 | (if (not color-identifiers:colorize-behavior) 657 | (progn 658 | (print "Major mode is not supported by color-identifiers, disabling") 659 | (color-identifiers-mode -1)) 660 | (unless color-identifiers:colors 661 | ;; When GUI emacs is used and additionally terminal-based `emacsclient' 662 | ;; gets launched, that may cause colors for identifiers change globally. 663 | ;; This is not a good user experience. Ideally we should keep two 664 | ;; separate vectors, for graphics and terminal versions of colors. But 665 | ;; it's unclear how that should communicate with theme changes, so unless 666 | ;; there's demand, let's for now just make sure we don't toggle colors 667 | ;; back and forth everywhere. 668 | (color-identifiers:regenerate-colors)) 669 | (when (null color-identifiers:color-index-for-identifier) 670 | (setq color-identifiers:color-index-for-identifier (make-hash-table :test 'equal))) 671 | (color-identifiers:refresh) 672 | (add-to-list 'font-lock-extra-managed-props 'color-identifiers:fontified) 673 | (font-lock-add-keywords nil '((color-identifiers:colorize . default)) t) 674 | (color-identifiers:enable-timer) 675 | (advice-add 'enable-theme :after #'color-identifiers:regen-on-theme-change))) 676 | (when color-identifiers:timer 677 | (cancel-timer color-identifiers:timer)) 678 | (font-lock-remove-keywords nil '((color-identifiers:colorize . default))) 679 | (advice-remove 'enable-theme #'color-identifiers:regen-on-theme-change)) 680 | (color-identifiers:refontify)) 681 | 682 | ;;;###autoload 683 | (define-globalized-minor-mode global-color-identifiers-mode 684 | color-identifiers-mode color-identifiers-mode-maybe) 685 | 686 | (defun color-identifiers:attribute-luminance (attribute) 687 | "Find the HSL luminance of the specified ATTRIBUTE on the default face." 688 | (let ((rgb (color-name-to-rgb (face-attribute 'default attribute)))) 689 | (if rgb 690 | (nth 2 (apply 'color-rgb-to-hsl rgb)) 691 | 0.5))) 692 | 693 | (defun color-identifiers:attribute-lab (attribute) 694 | "Find the LAB color value of the specified ATTRIBUTE on the default face." 695 | (let ((rgb (color-name-to-rgb (face-attribute 'default attribute)))) 696 | (if rgb 697 | (apply 'color-srgb-to-lab rgb) 698 | '(0.0 0.0 0.0)))) 699 | 700 | (defun color-identifiers:foreground-lab (face) 701 | "Find the LAB color value of the foreground attribute on the 702 | specified face." 703 | (let ((rgb (color-name-to-rgb (face-attribute face :foreground)))) 704 | (if rgb 705 | (apply 'color-srgb-to-lab rgb) 706 | '(0.0 0.0 0.0)))) 707 | 708 | (defun color-identifiers:refresh () 709 | "Refresh the set of identifiers in the current buffer. 710 | If `color-identifiers-coloring-method' is `sequential', 711 | identifiers and their corresponding color indexes are saved to 712 | `color-identifiers:color-index-for-identifier'. 713 | 714 | If `color-identifiers-coloring-method' is `hash' and a 715 | declaration scan function is registered for the current buffer's 716 | major mode, identifiers are saved to 717 | `color-identifiers:identifiers'." 718 | (interactive) 719 | (when color-identifiers-mode 720 | (cond 721 | ((eq color-identifiers-coloring-method 'sequential) 722 | (let ((i 0) 723 | ;; to make sure subsequently added vars aren't colorized the same add a (point) 724 | (randomize-subseq-calls (point))) 725 | (dolist (identifier (color-identifiers:list-identifiers)) 726 | (unless (gethash identifier color-identifiers:color-index-for-identifier) 727 | (puthash identifier 728 | (% (+ randomize-subseq-calls i) color-identifiers:num-colors) 729 | color-identifiers:color-index-for-identifier) 730 | (setq i (1+ i)))))) 731 | ((and (eq color-identifiers-coloring-method 'hash) 732 | (color-identifiers:get-declaration-scan-fn major-mode)) 733 | (setq color-identifiers:identifiers 734 | (color-identifiers:list-identifiers)))) 735 | (color-identifiers:refontify))) 736 | 737 | (defun color-identifiers:list-identifiers () 738 | "Return all identifiers in the current buffer." 739 | (if (color-identifiers:get-declaration-scan-fn major-mode) 740 | (funcall (color-identifiers:get-declaration-scan-fn major-mode)) 741 | ;; When no scan function is registered, fall back to 742 | ;; `color-identifiers:get-declarations', which returns all identifiers 743 | (color-identifiers:get-declarations))) 744 | 745 | (defalias 'color-identifiers:refontify 746 | (if (fboundp 'font-lock-flush) 747 | 'font-lock-flush 748 | (lambda () 749 | "Refontify the buffer using font-lock." 750 | (with-no-warnings 751 | (and font-lock-mode (font-lock-fontify-buffer)))))) 752 | 753 | (defun color-identifiers:color-identifier (identifier) 754 | "Return the hex color for IDENTIFIER, or nil if it should not 755 | be colored." 756 | (cond 757 | ((eq color-identifiers-coloring-method 'sequential) 758 | (let ((index (gethash identifier color-identifiers:color-index-for-identifier))) 759 | (when index 760 | (aref color-identifiers:colors index)))) 761 | ((eq color-identifiers-coloring-method 'hash) 762 | ;; If there is a declaration scan function for this major mode, the 763 | ;; candidate identifier should only be colored if it is in the memoized list 764 | ;; of identifiers. Otherwise, it should be colored unconditionally. 765 | (when (or (not (color-identifiers:get-declaration-scan-fn major-mode)) 766 | (member identifier color-identifiers:identifiers)) 767 | (color-identifiers:hash-identifier identifier))))) 768 | 769 | (defun color-identifiers:hash-identifier (identifier) 770 | "Return a color for IDENTIFIER based on its hash." 771 | (aref color-identifiers:colors 772 | (% (abs (sxhash identifier)) color-identifiers:num-colors))) 773 | 774 | (defun color-identifiers:scan-identifiers (fn limit) 775 | "Run FN on all candidate identifiers from point up to LIMIT. 776 | 777 | Basically, this is the function that highlights all identifiers, with 778 | highlight being done by applying FN. 779 | 780 | Candidate identifiers are defined by `color-identifiers:modes-alist'." 781 | (let ((identifier-context-re (nth 1 color-identifiers:colorize-behavior)) 782 | (identifier-re (nth 2 color-identifiers:colorize-behavior)) 783 | (identifier-faces (color-identifiers:curr-identifier-faces)) 784 | (identifier-exclusion-re (nth 4 color-identifiers:colorize-behavior))) 785 | ;; Skip forward to the next identifier that matches all four conditions 786 | (condition-case nil 787 | (while (< (point) limit) 788 | (if (or (let ((curr-face (get-text-property (point) 'face))) 789 | (or 790 | ;; Note: if `curr-face' is nil, the memq will usually 791 | ;; succeed because nil is typically part of the list. 792 | (memq curr-face identifier-faces) 793 | ;; `font-lock-variable-use-face' isn't included in 794 | ;; `identifier-faces' due to the latter being also used for 795 | ;; initial scanning, so `font-lock-variable-use-face' being 796 | ;; there would be an overhead with no benefit. 797 | (eq curr-face 'font-lock-variable-use-face))) 798 | (let ((flface-prop (get-text-property (point) 'font-lock-face))) 799 | (and flface-prop (memq flface-prop identifier-faces)))) 800 | (if (and (looking-back identifier-context-re (line-beginning-position)) 801 | (or (not identifier-exclusion-re) (not (looking-at-p identifier-exclusion-re))) 802 | (looking-at identifier-re)) 803 | (progn 804 | ;; Found an identifier. Run `fn' on it 805 | (funcall fn (match-beginning 1) (match-end 1)) 806 | (goto-char (match-end 1))) 807 | (forward-char) 808 | (re-search-forward identifier-re limit) 809 | (goto-char (match-beginning 0))) 810 | (goto-char (next-property-change (point) nil limit)))) 811 | (search-failed nil)))) 812 | 813 | (defun color-identifiers:colorize (limit) 814 | (color-identifiers:scan-identifiers 815 | (lambda (start end) 816 | (let* ((identifier (buffer-substring-no-properties start end)) 817 | (hex (color-identifiers:color-identifier identifier))) 818 | (when hex 819 | (let ((face-spec (append `(:foreground ,hex) color-identifiers:extra-face-attributes))) 820 | (add-text-properties start end `(face ,face-spec color-identifiers:fontified t)))))) 821 | limit)) 822 | 823 | (defun color-identifiers-mode-maybe () 824 | "Potentially enable `color-identifiers-mode' in the current buffer. 825 | 826 | Specifically, when `major-mode' is listed in 827 | `color-identifiers:modes-alist', and the buffer isn't temporary." 828 | ;; Avoid running in temp buffers created by `with-temp-buffer'. Most notably, 829 | ;; package installation process is known to create one and then enable 830 | ;; `emacs-lisp-mode' for the purpose of parsing autoloads, which in turn triggers 831 | ;; color-identifers-mode for no reason. During a huge upgrade this may add up, 832 | ;; especially given our ad hoc ELisp parsing code isn't the fastest. 833 | (when (and (not (string-prefix-p " *temp" (buffer-name))) 834 | (assoc major-mode color-identifiers:modes-alist)) 835 | (color-identifiers-mode 1))) 836 | 837 | (provide 'color-identifiers-mode) 838 | 839 | ;;; color-identifiers-mode.el ends here 840 | -------------------------------------------------------------------------------- /tests.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t -*- 2 | (require 'ert) 3 | (require 'color-identifiers-mode) 4 | 5 | (defvar color-identifiers:c-mode-text 6 | ["struct { 7 | int struct_a; 8 | char struct_b; 9 | } MyStruct; 10 | 11 | int main() { 12 | int main_a = 1; 13 | main_a = 7; 14 | // main_p main_a 15 | int *main_p = &main_a; 16 | }" (("struct_a" . 1) ("struct_b" . 1) ("MyStruct" . 1) ("main_a" . 3) ("main_p" . 1)) 17 | 18 | ;; test inserting/updating content 19 | " int main_a;" 20 | (("struct_a" . 1) ("struct_b" . 1) ("MyStruct" . 1) ("main_a" . 4) ("main_p" . 1))]) 21 | 22 | (defvar color-identifiers:elisp-mode-text 23 | ["(defun f (var1 var2) 24 | (+ var1 var2) 25 | (let ((var3 1)) 26 | ;; var1 var2 27 | (1+ var3)))" (("var1" . 2) ("var2" . 2) ("var3" . 2)) 28 | 29 | ;; test inserting/updating content 30 | " 31 | (setq var1 var3)" (("var1" . 3) ("var2" . 2) ("var3" . 3))]) 32 | 33 | (defvar color-identifiers:python-mode-text 34 | ["def f(arg1, arg2: int): 35 | # arg1 arg2 36 | sum(foo.arg1) + sum(arg2) # in foo.bar the bar is excluded from highlight 37 | arg3 = arg1 + arg2" (("arg1" . 2) ("arg2" . 3) ("arg3" . 1)) 38 | 39 | ;; test inserting/updating content 40 | " 41 | return arg1 + arg2" (("arg1" . 3) ("arg2" . 4) ("arg3" . 1))]) 42 | 43 | (defun color-identifiers:init-hash-table (list) 44 | "Initializes a hash-table with (key . val) pairs from list" 45 | (let ((table (make-hash-table :test 'equal))) 46 | ;; TODO: can't seem to find an easier way to initialize a hash-table 47 | (dolist (elem list) 48 | (puthash (car elem) (cdr elem) table)) 49 | table)) 50 | 51 | (defun color-identifiers:we-fontified-point () 52 | (get-text-property (point) 'color-identifiers:fontified)) 53 | 54 | (defun is-in-comment () 55 | "tests if point is in comment" 56 | (nth 4 (syntax-ppss))) 57 | 58 | (defun color-identifiers:all-identifiers-highlighted (ids) 59 | "Test that all identifiers in `ids' are highlighted in the buffer" 60 | (goto-char 1) 61 | (let ((highlights (make-hash-table :test 'equal)) 62 | (next-change (next-property-change (point-min))) 63 | (identifier-context-re (nth 1 color-identifiers:colorize-behavior)) 64 | (identifier-re (nth 2 color-identifiers:colorize-behavior)) 65 | (identifier-exclusion-re (nth 4 color-identifiers:colorize-behavior))) 66 | (while next-change 67 | (goto-char next-change) 68 | (let ((context-matches 69 | (and (looking-back identifier-context-re (line-beginning-position)) 70 | (or (not identifier-exclusion-re) (not (looking-at identifier-exclusion-re))))) 71 | (maybe-id (progn 72 | (if (looking-at identifier-re) 73 | (buffer-substring-no-properties (match-beginning 1) 74 | (match-end 1)) 75 | nil)))) 76 | (if (color-identifiers:we-fontified-point) 77 | (progn 78 | (should context-matches) 79 | (should maybe-id) 80 | (should (gethash maybe-id ids)) 81 | ;; increase match counter of the identifier found 82 | (let ((maybe-id-count (gethash maybe-id highlights))) 83 | (if maybe-id-count 84 | (puthash maybe-id (1+ maybe-id-count) highlights) 85 | (puthash maybe-id 1 highlights)))) 86 | (when (and context-matches maybe-id (not (is-in-comment))) 87 | (should (null (gethash maybe-id ids)))))) 88 | (setq next-change (next-property-change (point)))) 89 | ;; Now test that the amount of identifiers highlighted is as expected 90 | (maphash (lambda (expected-id expected-value) 91 | (let ((curr-highlight (gethash expected-id highlights))) 92 | (should curr-highlight) 93 | (should (= curr-highlight expected-value)))) 94 | ids))) 95 | 96 | (defun color-identifiers:test-mode (mode-func text-to-test) 97 | "Creates a buffer with the text, enables a major mode with 98 | `mode-func', enables `color-identifers-mode', then checks that 99 | identifers are highlighted as expected" 100 | (let* ((initial-content (aref text-to-test 0)) 101 | (expected-initial-ids (aref text-to-test 1)) 102 | (expected-initial-ids-table (color-identifiers:init-hash-table expected-initial-ids)) 103 | (updated-content (aref text-to-test 2)) 104 | (expected-updated-ids (aref text-to-test 3)) 105 | (expected-updated-ids-table (color-identifiers:init-hash-table expected-updated-ids)) 106 | initial-fontification) 107 | (with-temp-buffer 108 | (insert initial-content) 109 | (funcall mode-func) 110 | ;; most modes require (font-lock-ensure) for highlight to appear 111 | (font-lock-ensure) 112 | (color-identifiers-mode 1) 113 | ;; color-identifiers:scan-identifiers is called by font-lock when it considers 114 | ;; appropriate, so force it. 115 | (font-lock-ensure) 116 | (color-identifiers:all-identifiers-highlighted expected-initial-ids-table) 117 | 118 | ;; now test adding new content 119 | (setq initial-fontification `[,(buffer-substring (point-min) (point-max)) 120 | ,(point-min) 121 | ,(point-max)]) 122 | (goto-char (point-max)) 123 | (insert updated-content) 124 | (font-lock-ensure) ;; update highlight 125 | (color-identifiers:all-identifiers-highlighted expected-updated-ids-table) 126 | 127 | ;; check that we didn't change colors in the older part of the buffer 128 | 129 | ;; TODO: the emacs-version check works around a bug 130 | ;; https://emacs.stackexchange.com/a/42317/2671 But we can change the code to 131 | ;; iterate over the region and just create a list of our own properties, which 132 | ;; would work on all Emacs versions. It might even be more robust as we only 133 | ;; care of our changes and not the ones a mode may have made. 134 | (when (> emacs-major-version 28) 135 | (should (equal-including-properties 136 | (aref initial-fontification 0) 137 | (buffer-substring (aref initial-fontification 1) 138 | (aref initial-fontification 2)))))))) 139 | 140 | (ert-deftest test-c-mode-sequential () 141 | (setq color-identifiers-coloring-method 'sequential) 142 | (color-identifiers:test-mode #'c-mode color-identifiers:c-mode-text)) 143 | 144 | (ert-deftest test-emacs-lisp-mode-sequential () 145 | (setq color-identifiers-coloring-method 'sequential) 146 | (color-identifiers:test-mode 147 | #'emacs-lisp-mode color-identifiers:elisp-mode-text)) 148 | 149 | (ert-deftest test-python-mode-sequential () 150 | (setq color-identifiers-coloring-method 'sequential) 151 | (color-identifiers:test-mode #'python-mode color-identifiers:python-mode-text)) 152 | 153 | (ert-deftest test-c-mode-hash () 154 | (setq color-identifiers-coloring-method 'hash) 155 | (color-identifiers:test-mode #'c-mode color-identifiers:c-mode-text)) 156 | 157 | (ert-deftest test-emacs-lisp-mode-hash () 158 | (setq color-identifiers-coloring-method 'hash) 159 | (color-identifiers:test-mode 160 | #'emacs-lisp-mode color-identifiers:elisp-mode-text)) 161 | 162 | (ert-deftest test-python-mode-hash () 163 | (setq color-identifiers-coloring-method 'hash) 164 | (color-identifiers:test-mode #'python-mode color-identifiers:python-mode-text)) 165 | --------------------------------------------------------------------------------