├── .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 | (color-identifiers:set-declaration-scan-fn 503 | 'clojure-mode 'color-identifiers:clojure-get-declarations) 504 | 505 | (add-to-list 506 | 'color-identifiers:modes-alist 507 | `(clojure-mode . ("" 508 | "\\_<\\(\\(?:\\s_\\|\\sw\\)+\\)" 509 | (nil)))) 510 | 511 | (add-to-list 512 | 'color-identifiers:modes-alist 513 | `(clojurescript-mode . ("" 514 | "\\_<\\(\\(?:\\s_\\|\\sw\\)+\\)" 515 | (nil)))) 516 | 517 | (dolist (maj-mode '(tuareg-mode sml-mode)) 518 | (add-to-list 519 | 'color-identifiers:modes-alist 520 | `(,maj-mode . ("" 521 | "\\_<\\([a-zA-Z_$]\\(?:\\s_\\|\\sw\\|'\\)*\\)" 522 | (nil font-lock-variable-name-face))))) 523 | 524 | ;; R support in ess-mode 525 | (defun color-identifiers:remove-string-or-comment (str) 526 | "Remove string or comment in str, based on font lock faces" 527 | (let ((remove (memq (get-text-property 0 'face str) 528 | '(font-lock-string-face font-lock-comment-face))) 529 | (pos 0) 530 | (nextpos) 531 | (result "")) 532 | (while (setq nextpos (next-single-property-change pos 'face str)) 533 | (unless remove 534 | (setq result (concat result (substring-no-properties str pos nextpos)))) 535 | (setq pos nextpos) 536 | (setq remove (memq (get-text-property pos 'face str) 537 | '(font-lock-string-face font-lock-comment-face)))) 538 | (unless remove 539 | (setq result (concat result (substring-no-properties str pos nextpos)))) 540 | result)) 541 | 542 | (defun color-identifiers:r-get-args (lend) 543 | "Extract a list of function arg names. LEND is the point at 544 | the left parenthesis, after `function' keyword." 545 | (let* ((rend (save-excursion 546 | (goto-char lend) 547 | (forward-sexp) 548 | (point))) 549 | (str (color-identifiers:remove-string-or-comment 550 | (buffer-substring (1+ lend) (1- rend))))) 551 | (mapcar (lambda (s) (replace-regexp-in-string "\\s *=.*" "" s)) 552 | (split-string str "," t " ")))) 553 | 554 | (defun color-identifiers:r-get-declarations () 555 | "Extract a list of identifiers declared in the current buffer. 556 | For Emacs Lisp support within color-identifiers-mode." 557 | (let ((result nil)) 558 | (save-excursion 559 | (goto-char (point-min)) 560 | (while (re-search-forward "\\(\\(?:\\w\\|\\s_\\)*\\)\\s *< (cdr x) (cdr y))) min-dists))) 632 | (funcall choose-candidate (car best)))) 633 | (setq color-identifiers:colors 634 | (vconcat (-map (lambda (lab) 635 | (let* ((srgb (apply 'color-lab-to-srgb lab)) 636 | (rgb (mapcar 'color-clamp srgb))) 637 | (apply 'color-rgb-to-hex rgb))) 638 | chosens)))))) 639 | 640 | (defvar-local color-identifiers:color-index-for-identifier nil 641 | "Hashtable of identifier-index pairs for internal use. 642 | The index refers to `color-identifiers:colors'. Only used when 643 | `color-identifiers-coloring-method' is `sequential'.") 644 | 645 | (defvar-local color-identifiers:identifiers nil 646 | "Set of identifiers in the current buffer. 647 | Only used when `color-identifiers-coloring-method' is `hash' and 648 | a declaration scan function is registered for the current major 649 | mode. This variable memoizes the result of the declaration scan function.") 650 | 651 | ;;;###autoload 652 | (define-minor-mode color-identifiers-mode 653 | "Color the identifiers in the current buffer based on their names." 654 | :init-value nil 655 | :lighter " ColorIds" 656 | (if color-identifiers-mode 657 | (progn 658 | (setq color-identifiers:colorize-behavior 659 | (assoc major-mode color-identifiers:modes-alist)) 660 | (if (not color-identifiers:colorize-behavior) 661 | (progn 662 | (print "Major mode is not supported by color-identifiers, disabling") 663 | (color-identifiers-mode -1)) 664 | (unless color-identifiers:colors 665 | ;; When GUI emacs is used and additionally terminal-based `emacsclient' 666 | ;; gets launched, that may cause colors for identifiers change globally. 667 | ;; This is not a good user experience. Ideally we should keep two 668 | ;; separate vectors, for graphics and terminal versions of colors. But 669 | ;; it's unclear how that should communicate with theme changes, so unless 670 | ;; there's demand, let's for now just make sure we don't toggle colors 671 | ;; back and forth everywhere. 672 | (color-identifiers:regenerate-colors)) 673 | (when (null color-identifiers:color-index-for-identifier) 674 | (setq color-identifiers:color-index-for-identifier (make-hash-table :test 'equal))) 675 | (color-identifiers:refresh) 676 | (add-to-list 'font-lock-extra-managed-props 'color-identifiers:fontified) 677 | (font-lock-add-keywords nil '((color-identifiers:colorize . default)) t) 678 | (color-identifiers:enable-timer) 679 | (advice-add 'enable-theme :after #'color-identifiers:regen-on-theme-change))) 680 | (when color-identifiers:timer 681 | (cancel-timer color-identifiers:timer)) 682 | (font-lock-remove-keywords nil '((color-identifiers:colorize . default))) 683 | (advice-remove 'enable-theme #'color-identifiers:regen-on-theme-change)) 684 | (color-identifiers:refontify)) 685 | 686 | ;;;###autoload 687 | (define-globalized-minor-mode global-color-identifiers-mode 688 | color-identifiers-mode color-identifiers-mode-maybe) 689 | 690 | (defun color-identifiers:attribute-luminance (attribute) 691 | "Find the HSL luminance of the specified ATTRIBUTE on the default face." 692 | (let ((rgb (color-name-to-rgb (face-attribute 'default attribute)))) 693 | (if rgb 694 | (nth 2 (apply 'color-rgb-to-hsl rgb)) 695 | 0.5))) 696 | 697 | (defun color-identifiers:attribute-lab (attribute) 698 | "Find the LAB color value of the specified ATTRIBUTE on the default face." 699 | (let ((rgb (color-name-to-rgb (face-attribute 'default attribute)))) 700 | (if rgb 701 | (apply 'color-srgb-to-lab rgb) 702 | '(0.0 0.0 0.0)))) 703 | 704 | (defun color-identifiers:foreground-lab (face) 705 | "Find the LAB color value of the foreground attribute on the 706 | specified face." 707 | (let ((rgb (color-name-to-rgb (face-attribute face :foreground)))) 708 | (if rgb 709 | (apply 'color-srgb-to-lab rgb) 710 | '(0.0 0.0 0.0)))) 711 | 712 | (defun color-identifiers:refresh () 713 | "Refresh the set of identifiers in the current buffer. 714 | If `color-identifiers-coloring-method' is `sequential', 715 | identifiers and their corresponding color indexes are saved to 716 | `color-identifiers:color-index-for-identifier'. 717 | 718 | If `color-identifiers-coloring-method' is `hash' and a 719 | declaration scan function is registered for the current buffer's 720 | major mode, identifiers are saved to 721 | `color-identifiers:identifiers'." 722 | (interactive) 723 | (when color-identifiers-mode 724 | (cond 725 | ((eq color-identifiers-coloring-method 'sequential) 726 | (let ((i 0) 727 | ;; to make sure subsequently added vars aren't colorized the same add a (point) 728 | (randomize-subseq-calls (point))) 729 | (dolist (identifier (color-identifiers:list-identifiers)) 730 | (unless (gethash identifier color-identifiers:color-index-for-identifier) 731 | (puthash identifier 732 | (% (+ randomize-subseq-calls i) color-identifiers:num-colors) 733 | color-identifiers:color-index-for-identifier) 734 | (setq i (1+ i)))))) 735 | ((and (eq color-identifiers-coloring-method 'hash) 736 | (color-identifiers:get-declaration-scan-fn major-mode)) 737 | (setq color-identifiers:identifiers 738 | (color-identifiers:list-identifiers)))) 739 | (color-identifiers:refontify))) 740 | 741 | (defun color-identifiers:list-identifiers () 742 | "Return all identifiers in the current buffer." 743 | (if (color-identifiers:get-declaration-scan-fn major-mode) 744 | (funcall (color-identifiers:get-declaration-scan-fn major-mode)) 745 | ;; When no scan function is registered, fall back to 746 | ;; `color-identifiers:get-declarations', which returns all identifiers 747 | (color-identifiers:get-declarations))) 748 | 749 | (defalias 'color-identifiers:refontify 750 | (if (fboundp 'font-lock-flush) 751 | 'font-lock-flush 752 | (lambda () 753 | "Refontify the buffer using font-lock." 754 | (with-no-warnings 755 | (and font-lock-mode (font-lock-fontify-buffer)))))) 756 | 757 | (defun color-identifiers:color-identifier (identifier) 758 | "Return the hex color for IDENTIFIER, or nil if it should not 759 | be colored." 760 | (cond 761 | ((eq color-identifiers-coloring-method 'sequential) 762 | (let ((index (gethash identifier color-identifiers:color-index-for-identifier))) 763 | (when index 764 | (aref color-identifiers:colors index)))) 765 | ((eq color-identifiers-coloring-method 'hash) 766 | ;; If there is a declaration scan function for this major mode, the 767 | ;; candidate identifier should only be colored if it is in the memoized list 768 | ;; of identifiers. Otherwise, it should be colored unconditionally. 769 | (when (or (not (color-identifiers:get-declaration-scan-fn major-mode)) 770 | (member identifier color-identifiers:identifiers)) 771 | (color-identifiers:hash-identifier identifier))))) 772 | 773 | (defun color-identifiers:hash-identifier (identifier) 774 | "Return a color for IDENTIFIER based on its hash." 775 | (aref color-identifiers:colors 776 | (% (abs (sxhash identifier)) color-identifiers:num-colors))) 777 | 778 | (defun color-identifiers:scan-identifiers (fn limit) 779 | "Run FN on all candidate identifiers from point up to LIMIT. 780 | 781 | Basically, this is the function that highlights all identifiers, with 782 | highlight being done by applying FN. 783 | 784 | Candidate identifiers are defined by `color-identifiers:modes-alist'." 785 | (let ((identifier-context-re (nth 1 color-identifiers:colorize-behavior)) 786 | (identifier-re (nth 2 color-identifiers:colorize-behavior)) 787 | (identifier-faces (color-identifiers:curr-identifier-faces)) 788 | (identifier-exclusion-re (nth 4 color-identifiers:colorize-behavior))) 789 | ;; Skip forward to the next identifier that matches all four conditions 790 | (condition-case nil 791 | (while (< (point) limit) 792 | (if (or (let ((curr-face (get-text-property (point) 'face))) 793 | (or 794 | ;; Note: if `curr-face' is nil, the memq will usually 795 | ;; succeed because nil is typically part of the list. 796 | (memq curr-face identifier-faces) 797 | ;; `font-lock-variable-use-face' isn't included in 798 | ;; `identifier-faces' due to the latter being also used for 799 | ;; initial scanning, so `font-lock-variable-use-face' being 800 | ;; there would be an overhead with no benefit. 801 | (eq curr-face 'font-lock-variable-use-face))) 802 | (let ((flface-prop (get-text-property (point) 'font-lock-face))) 803 | (and flface-prop (memq flface-prop identifier-faces)))) 804 | (if (and (looking-back identifier-context-re (line-beginning-position)) 805 | (or (not identifier-exclusion-re) (not (looking-at-p identifier-exclusion-re))) 806 | (looking-at identifier-re)) 807 | (progn 808 | ;; Found an identifier. Run `fn' on it 809 | (funcall fn (match-beginning 1) (match-end 1)) 810 | (goto-char (match-end 1))) 811 | (forward-char) 812 | (re-search-forward identifier-re limit) 813 | (goto-char (match-beginning 0))) 814 | (goto-char (next-property-change (point) nil limit)))) 815 | (search-failed nil)))) 816 | 817 | (defun color-identifiers:colorize (limit) 818 | (color-identifiers:scan-identifiers 819 | (lambda (start end) 820 | (let* ((identifier (buffer-substring-no-properties start end)) 821 | (hex (color-identifiers:color-identifier identifier))) 822 | (when hex 823 | (let ((face-spec (append `(:foreground ,hex) color-identifiers:extra-face-attributes))) 824 | (add-text-properties start end `(face ,face-spec color-identifiers:fontified t)))))) 825 | limit)) 826 | 827 | (defun color-identifiers-mode-maybe () 828 | "Potentially enable `color-identifiers-mode' in the current buffer. 829 | 830 | Specifically, when `major-mode' is listed in 831 | `color-identifiers:modes-alist', and the buffer isn't temporary." 832 | ;; Avoid running in temp buffers created by `with-temp-buffer'. Most notably, 833 | ;; package installation process is known to create one and then enable 834 | ;; `emacs-lisp-mode' for the purpose of parsing autoloads, which in turn triggers 835 | ;; color-identifers-mode for no reason. During a huge upgrade this may add up, 836 | ;; especially given our ad hoc ELisp parsing code isn't the fastest. 837 | (when (and (not (string-prefix-p " *temp" (buffer-name))) 838 | (assoc major-mode color-identifiers:modes-alist)) 839 | (color-identifiers-mode 1))) 840 | 841 | (provide 'color-identifiers-mode) 842 | 843 | ;;; color-identifiers-mode.el ends here 844 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------