1 ;;; context-coloring.el --- Syntax highlighting, except not for syntax. -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2014-2015 Free Software Foundation, Inc.
5 ;; Author: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
6 ;; URL: https://github.com/jacksonrayhamilton/context-coloring
7 ;; Keywords: context coloring syntax highlighting
9 ;; Package-Requires: ((emacs "24") (js2-mode "20150126"))
11 ;; This file is part of GNU Emacs.
13 ;; This program is free software; you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation, either version 3 of the License, or
16 ;; (at your option) any later version.
18 ;; This program is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
28 ;; Highlights code according to function context.
30 ;; - Code in the global scope is one color. Code in functions within the global
31 ;; scope is a different color, and code within such functions is another
33 ;; - Identifiers retain the color of the scope in which they are declared.
35 ;; Lexical scope information at-a-glance can assist a programmer in
36 ;; understanding the overall structure of a program. It can help to curb nasty
37 ;; bugs like name shadowing. A rainbow can indicate excessive complexity. State
38 ;; change within a closure is easily monitored.
40 ;; By default, Context Coloring still highlights comments and strings
41 ;; syntactically. It is still easy to differentiate code from non-code, and
42 ;; strings cannot be confused for variables.
44 ;; To use, add the following to your ~/.emacs:
46 ;; (require 'context-coloring)
47 ;; (add-hook 'js2-mode-hook 'context-coloring-mode)
49 ;; js-mode or js3-mode support requires Node.js 0.10+ and the scopifier
52 ;; $ npm install -g scopifier
59 ;;; Customizable options
61 (defcustom context-coloring-delay 0.25
62 "Delay between a buffer update and colorization.
64 Increase this if your machine is high-performing. Decrease it if
67 Supported modes: `js-mode', `js3-mode'"
68 :group 'context-coloring)
70 (defcustom context-coloring-comments-and-strings t
71 "If non-nil, also color comments and strings using `font-lock'."
72 :group 'context-coloring)
74 (defcustom context-coloring-js-block-scopes nil
75 "If non-nil, also color block scopes in the scope hierarchy in JavaScript.
77 The block-scoped `let' and `const' are introduced in ES6. If you
78 are writing ES6 code, enable this; otherwise, don't.
80 Supported modes: `js2-mode'"
81 :group 'context-coloring)
83 (defcustom context-coloring-benchmark-colorization nil
84 "If non-nil, track how long colorization takes and print
85 messages with the colorization duration."
86 :group 'context-coloring)
91 (defvar-local context-coloring-buffer nil
92 "Reference to this buffer (for timers).")
94 (defvar-local context-coloring-scopifier-process nil
95 "Reference to the single scopifier process that can be
98 (defvar-local context-coloring-colorize-idle-timer nil
99 "Reference to the currently-running idle timer.")
101 (defvar-local context-coloring-changed nil
102 "Indication that the buffer has changed recently, which would
103 imply that it should be colorized again by
104 `context-coloring-colorize-idle-timer' if that timer is being
110 (defun context-coloring-defface (level tty light dark)
111 "Dynamically define a face for LEVEL with colors for TTY, LIGHT
112 and DARK backgrounds."
113 (let ((face (intern (format "context-coloring-level-%s-face" level)))
114 (doc (format "Context coloring face, level %s." level)))
118 '((((type tty)) (:foreground ,tty))
119 (((background light)) (:foreground ,light))
120 (((background dark)) (:foreground ,dark)))
122 :group 'context-coloring)))))
124 (defvar context-coloring-face-count nil
125 "Number of faces available for coloring.")
127 (defun context-coloring-defface-default (level)
128 "Define a face for LEVEL with the default neutral colors."
129 (context-coloring-defface level nil "#3f3f3f" "#cdcdcd"))
131 (defun context-coloring-set-colors-default ()
132 (context-coloring-defface 0 nil "#000000" "#ffffff")
133 (context-coloring-defface 1 "yellow" "#007f80" "#ffff80")
134 (context-coloring-defface 2 "green" "#001580" "#cdfacd")
135 (context-coloring-defface 3 "cyan" "#550080" "#d8d8ff")
136 (context-coloring-defface 4 "blue" "#802b00" "#e7c7ff")
137 (context-coloring-defface 5 "magenta" "#6a8000" "#ffcdcd")
138 (context-coloring-defface 6 "red" "#008000" "#ffe390")
139 (context-coloring-defface-default 7)
140 (setq context-coloring-face-count 8))
142 (context-coloring-set-colors-default)
144 ;; Color theme authors can have up to 26 levels: 1 (0th) for globals, 24
145 ;; (1st-24th) for in-betweens, and 1 (25th) for infinity.
147 (context-coloring-defface-default (+ number context-coloring-face-count)))
152 (defsubst context-coloring-face-symbol (level)
153 "Returns a symbol for a face with LEVEL."
154 ;; `concat' is faster than `format' here.
155 (intern-soft (concat "context-coloring-level-"
156 (number-to-string level)
159 (defun context-coloring-set-colors (&rest colors)
160 "Set context coloring's levels' coloring to COLORS, where the
161 Nth element of COLORS is level N's color."
162 (setq context-coloring-face-count (length colors))
164 (dolist (color colors)
165 ;; Ensure there are available faces to contain new colors.
166 (when (not (context-coloring-face-symbol level))
167 (context-coloring-defface-default level))
168 (set-face-foreground (context-coloring-face-symbol level) color)
169 (setq level (+ level 1)))))
171 (defsubst context-coloring-level-face (level)
172 "Returns the face name for LEVEL."
173 (context-coloring-face-symbol (min level context-coloring-face-count)))
176 ;;; Colorization utilities
178 (defsubst context-coloring-colorize-region (start end level)
179 "Colorizes characters from the 1-indexed START (inclusive) to
180 END (exclusive) with the face corresponding to LEVEL."
184 `(face ,(context-coloring-level-face level))))
186 (defsubst context-coloring-maybe-colorize-comments-and-strings ()
187 "Colorizes the current buffer's comments and strings if
188 `context-coloring-comments-and-strings' is non-nil."
189 (when context-coloring-comments-and-strings
191 (font-lock-fontify-syntactically-region (point-min) (point-max)))))
194 ;;; js2-mode colorization
196 (defvar-local context-coloring-js2-scope-level-hash-table nil
197 "Associates `js2-scope' structures and with their scope
200 (defsubst context-coloring-js2-scope-level (scope)
201 "Gets the level of SCOPE."
202 (cond ((gethash scope context-coloring-js2-scope-level-hash-table))
205 (current-scope scope)
207 (while (and current-scope
208 (js2-node-parent current-scope)
209 (setq enclosing-scope
210 (js2-node-get-enclosing-scope current-scope)))
211 (when (or context-coloring-js-block-scopes
212 (let ((type (js2-scope-type current-scope)))
213 (or (= type js2-SCRIPT)
214 (= type js2-FUNCTION)
215 (= type js2-CATCH))))
216 (setq level (+ level 1)))
217 (setq current-scope enclosing-scope))
218 (puthash scope level context-coloring-js2-scope-level-hash-table)))))
220 (defsubst context-coloring-js2-local-name-node-p (node)
221 "Determines if NODE is a js2-name-node representing a local
223 (and (js2-name-node-p node)
224 (let ((parent (js2-node-parent node)))
225 (not (or (and (js2-object-prop-node-p parent)
226 (eq node (js2-object-prop-node-left parent)))
227 (and (js2-prop-get-node-p parent)
228 ;; For nested property lookup, the node on the left is a
229 ;; `js2-prop-get-node', so this always works.
230 (eq node (js2-prop-get-node-right parent))))))))
232 (defsubst context-coloring-js2-colorize-node (node level)
233 "Colors NODE with the color for LEVEL."
234 (let ((start (js2-node-abs-pos node)))
235 (context-coloring-colorize-region
237 (+ start (js2-node-len node)) ; End
240 (defun context-coloring-js2-colorize ()
241 "Colorizes the current buffer using the abstract syntax tree
242 generated by js2-mode."
243 ;; Reset the hash table; the old one could be obsolete.
244 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test 'eq))
245 (with-silent-modifications
252 (context-coloring-js2-colorize-node
254 (context-coloring-js2-scope-level node)))
255 ((context-coloring-js2-local-name-node-p node)
256 (let* ((enclosing-scope (js2-node-get-enclosing-scope node))
257 (defining-scope (js2-get-defining-scope
259 (js2-name-node-name node))))
260 ;; The tree seems to be walked lexically, so an entire scope will
261 ;; be colored, including its name nodes, before they are reached.
262 ;; Coloring the nodes defined in that scope would be redundant, so
264 (when (not (eq defining-scope enclosing-scope))
265 (context-coloring-js2-colorize-node
267 (context-coloring-js2-scope-level defining-scope))))))
268 ;; The `t' indicates to search children.
270 (context-coloring-maybe-colorize-comments-and-strings)))
273 ;;; Shell command scopification / colorization
275 (defun context-coloring-apply-tokens (tokens)
276 "Processes a vector of TOKENS to apply context-based coloring
277 to the current buffer. Tokens are 3 integers: start, end, level.
278 The vector is flat, with a new token occurring after every 3rd
280 (with-silent-modifications
282 (len (length tokens)))
284 (context-coloring-colorize-region
287 (elt tokens (+ i 2)))
289 (context-coloring-maybe-colorize-comments-and-strings)))
291 (defun context-coloring-parse-array (input)
292 "Specialized JSON parser for a flat array of numbers."
294 (mapcar 'string-to-number (split-string (substring input 1 -1) ","))))
296 (defun context-coloring-kill-scopifier ()
297 "Kills the currently-running scopifier process for this
299 (when (not (null context-coloring-scopifier-process))
300 (delete-process context-coloring-scopifier-process)
301 (setq context-coloring-scopifier-process nil)))
303 (defun context-coloring-scopify-shell-command (command &optional callback)
304 "Invokes a scopifier with the current buffer's contents,
305 reading the scopifier's response asynchronously and applying a
306 parsed list of tokens to `context-coloring-apply-tokens'.
308 Invokes CALLBACK when complete."
310 ;; Prior running tokenization is implicitly obsolete if this function is
312 (context-coloring-kill-scopifier)
314 ;; Start the process.
315 (setq context-coloring-scopifier-process
316 (start-process-shell-command "scopifier" nil command))
319 (buffer context-coloring-buffer))
321 ;; The process may produce output in multiple chunks. This filter
322 ;; accumulates the chunks into a message.
324 context-coloring-scopifier-process
325 (lambda (_process chunk)
326 (setq output (concat output chunk))))
328 ;; When the process's message is complete, this sentinel parses it as JSON
329 ;; and applies the tokens to the buffer.
330 (set-process-sentinel
331 context-coloring-scopifier-process
332 (lambda (_process event)
333 (when (equal "finished\n" event)
334 (let ((tokens (context-coloring-parse-array output)))
335 (with-current-buffer buffer
336 (context-coloring-apply-tokens tokens))
337 (setq context-coloring-scopifier-process nil)
338 (if callback (funcall callback)))))))
340 ;; Give the process its input so it can begin.
342 context-coloring-scopifier-process
343 (point-min) (point-max))
345 context-coloring-scopifier-process))
350 (defvar context-coloring-dispatch-hash-table (make-hash-table :test 'eq)
351 "Mapping of dispatch strategy names to their corresponding
352 property lists, which contain details about the strategies.")
354 (defvar context-coloring-mode-hash-table (make-hash-table :test 'eq)
355 "Mapping of major mode names to dispatch property lists.")
357 (defun context-coloring-select-dispatch (mode dispatch)
358 "Use DISPATCH for MODE."
363 context-coloring-dispatch-hash-table)
364 context-coloring-mode-hash-table))
366 (defun context-coloring-define-dispatch (symbol &rest properties)
367 "Define a new dispatch named SYMBOL with PROPERTIES.
369 A \"dispatch\" is a property list describing a strategy for
370 coloring a buffer. There are three possible strategies: Parse
371 and color in a single function (`:colorizer'), parse in a
372 function that returns scope data (`:scopifier'), or parse with a
373 shell command that returns scope data (`:command'). In the
374 latter two cases, the scope data will be used to automatically
377 PROPERTIES must include `:modes' and one of `:colorizer',
378 `:scopifier' or `:command'.
380 `:modes' - List of major modes this dispatch is valid for.
382 `:colorizer' - Symbol referring to a function that parses and
385 `:scopifier' - Symbol referring to a function that parses the
386 buffer a returns a flat vector of start, end and level data.
388 `:executable' - Optional name of an executable required by
391 `:command' - Shell command to execute with the current buffer
392 sent via stdin, and with a flat JSON array of start, end and
393 level data returned via stdout."
394 (let ((modes (plist-get properties :modes))
395 (colorizer (plist-get properties :colorizer))
396 (scopifier (plist-get properties :scopifier))
397 (command (plist-get properties :command)))
399 (error "No mode defined for dispatch"))
400 (when (not (or colorizer
403 (error "No colorizer, scopifier or command defined for dispatch"))
404 (puthash symbol properties context-coloring-dispatch-hash-table)
406 (when (null (gethash mode context-coloring-mode-hash-table))
407 (puthash mode properties context-coloring-mode-hash-table)))))
409 (context-coloring-define-dispatch
411 :modes '(js-mode js3-mode)
412 :executable "scopifier"
413 :command "scopifier")
415 (context-coloring-define-dispatch
418 :colorizer 'context-coloring-js2-colorize)
420 (defun context-coloring-dispatch (&optional callback)
421 "Determines the optimal track for scopification / colorization
422 of the current buffer, then executes it.
424 Invokes CALLBACK when complete. It is invoked synchronously for
425 elisp tracks, and asynchronously for shell command tracks."
426 (let ((dispatch (gethash major-mode context-coloring-mode-hash-table)))
428 (message "%s" "Context coloring is not available for this major mode"))
434 ((setq colorizer (plist-get dispatch :colorizer))
436 (if callback (funcall callback)))
437 ((setq scopifier (plist-get dispatch :scopifier))
438 (context-coloring-apply-tokens (funcall scopifier))
439 (if callback (funcall callback)))
440 ((setq command (plist-get dispatch :command))
441 (setq executable (plist-get dispatch :executable))
443 (null (executable-find executable)))
444 (message "Executable \"%s\" not found" executable)
445 (context-coloring-scopify-shell-command command callback)))))))
450 (defun context-coloring-colorize (&optional callback)
451 "Colors the current buffer by function context.
453 Invokes CALLBACK when complete; see `context-coloring-dispatch'."
455 (let ((start-time (float-time)))
456 (context-coloring-dispatch
458 (when context-coloring-benchmark-colorization
459 (message "Colorization took %.3f seconds" (- (float-time) start-time)))
460 (if callback (funcall callback))))))
462 (defun context-coloring-change-function (_start _end _length)
463 "Registers a change so that a buffer can be colorized soon."
464 ;; Tokenization is obsolete if there was a change.
465 (context-coloring-kill-scopifier)
466 (setq context-coloring-changed t))
468 (defun context-coloring-maybe-colorize ()
469 "Colorize unders certain conditions. This will run as an idle
470 timer, so firstly the buffer must not be some other buffer.
471 Additionally, the buffer must have changed, otherwise colorizing
473 (when (and (eq context-coloring-buffer (window-buffer (selected-window)))
474 context-coloring-changed)
475 (setq context-coloring-changed nil)
476 (context-coloring-colorize)))
481 (defvar context-coloring-theme-hash-table (make-hash-table :test 'eq)
482 "Mapping of theme names to theme properties.")
484 (defun context-coloring-theme-p (theme)
485 "Return t if THEME is defined, nil otherwise."
486 (and (gethash theme context-coloring-theme-hash-table)))
488 (defconst context-coloring-level-face-regexp
489 "context-coloring-level-\\([[:digit:]]+\\)-face"
490 "Regular expression for extracting a level from a face.")
492 (defvar context-coloring-originally-set-theme-hash-table
493 (make-hash-table :test 'eq)
494 "Cache of custom themes who originally set their own
495 `context-coloring-level-N-face' faces.")
497 (defun context-coloring-theme-originally-set-p (theme)
498 "Return t if there is a `context-coloring-level-N-face'
499 originally set for THEME, nil otherwise."
500 (let (originally-set)
502 ;; `setq' might return a non-nil value for the sake of this `cond'.
507 context-coloring-originally-set-theme-hash-table))
508 (eq originally-set 'yes))
510 (let* ((settings (get theme 'theme-settings))
513 (while (and tail (not found))
514 (and (eq (nth 0 (car tail)) 'theme-face)
516 context-coloring-level-face-regexp
517 (symbol-name (nth 1 (car tail))))
519 (setq tail (cdr tail)))
522 (defun context-coloring-cache-originally-set (theme originally-set)
523 "Remember if THEME had colors originally set for it; if
524 ORIGINALLY-SET is non-nil, it did, otherwise it didn't."
525 ;; Caching whether a theme was originally set is kind of dirty, but we have to
526 ;; do it to remember the past state of the theme. There are probably some
527 ;; edge cases where caching will be an issue, but they are probably rare.
530 (if originally-set 'yes 'no)
531 context-coloring-originally-set-theme-hash-table))
533 (defun context-coloring-warn-theme-originally-set (theme)
534 "Warns the user that the colors for a theme are already
536 (warn "Context coloring colors for theme `%s' are already defined" theme))
538 (defun context-coloring-theme-highest-level (theme)
539 "Return the highest level N of a face like
540 `context-coloring-level-N-face' set for THEME, or -1 if there is
542 (let* ((settings (get theme 'theme-settings))
548 (and (eq (nth 0 (car tail)) 'theme-face)
549 (setq face-string (symbol-name (nth 1 (car tail))))
551 context-coloring-level-face-regexp
553 (setq number (string-to-number
554 (substring face-string
559 (setq tail (cdr tail)))
562 (defun context-coloring-apply-theme (theme)
563 "Applies THEME's properties to its respective custom theme,
564 which must already exist and which *should* already be enabled."
565 (let* ((properties (gethash theme context-coloring-theme-hash-table))
566 (colors (plist-get properties :colors))
568 (setq context-coloring-face-count (length colors))
570 'custom-theme-set-faces
574 (setq level (+ level 1))
575 `(,(context-coloring-face-symbol level) ((t (:foreground ,color)))))
578 (defun context-coloring-define-theme (theme &rest properties)
579 "Define a context theme named THEME for coloring scope levels.
581 PROPERTIES is a property list specifiying the following details:
583 `:aliases': List of symbols of other custom themes that these
584 colors are applicable to.
586 `:colors': List of colors that this context theme uses.
588 `:override': If non-nil, this context theme is intentionally
589 overriding colors set by a custom theme. Don't set this non-nil
590 unless there is a custom theme you want to use which sets
591 `context-coloring-level-N-face' faces that you want to replace.
593 `:recede': If non-nil, this context theme should not apply its
594 colors if a custom theme already sets
595 `context-coloring-level-N-face' faces. This option is
596 optimistic; set this non-nil if you would rather confer the duty
597 of picking colors to a custom theme author (if / when he ever
600 By default, context themes will always override custom themes,
601 even if those custom themes set `context-coloring-level-N-face'
602 faces. If a context theme does override a custom theme, a
603 warning will be raised, at which point you may want to enable the
604 `:override' option, or just delete your context theme and opt to
605 use your custom theme's author's colors instead.
607 Context themes only work for the custom theme with the highest
608 precedence, i.e. the car of `custom-enabled-themes'."
609 (let ((aliases (plist-get properties :aliases))
610 (override (plist-get properties :override))
611 (recede (plist-get properties :recede)))
612 (dolist (name (append `(,theme) aliases))
613 (puthash name properties context-coloring-theme-hash-table)
614 (when (custom-theme-p name)
615 (let ((originally-set (context-coloring-theme-originally-set-p name)))
616 (context-coloring-cache-originally-set name originally-set)
617 ;; In the particular case when you innocently define colors that a
618 ;; custom theme originally set, warn. Arguably this only has to be
619 ;; done at enable time, but it is probably more useful to do it at
620 ;; definition time for prompter feedback.
621 (when (and originally-set
624 (context-coloring-warn-theme-originally-set name))
625 ;; Set (or overwrite) colors.
626 (when (not (and originally-set
628 (context-coloring-apply-theme name)))))))
630 (defun context-coloring-load-theme (&optional rest)
633 "context themes are now loaded alongside custom themes automatically."
636 (defun context-coloring-enable-theme (theme)
637 "Applies THEME if its colors are not already set, else just
638 sets `context-coloring-face-count' to the correct value for
640 (let* ((properties (gethash theme context-coloring-theme-hash-table))
641 (recede (plist-get properties :recede))
642 (override (plist-get properties :override)))
645 (let ((highest-level (context-coloring-theme-highest-level theme)))
647 ;; This can be true whether originally set by a custom theme or by a
649 ((> highest-level -1)
650 (setq context-coloring-face-count (+ highest-level 1)))
651 ;; It is possible that the corresponding custom theme did not exist at
652 ;; the time of defining this context theme, and in that case the above
653 ;; condition proves the custom theme did not originally set any faces,
654 ;; so we have license to apply the context theme for the first time
657 (context-coloring-apply-theme theme)))))
659 (let ((originally-set (context-coloring-theme-originally-set-p theme)))
660 ;; Cache now in case the context theme was defined after the custom
662 (context-coloring-cache-originally-set theme originally-set)
663 (when (and originally-set
665 (context-coloring-warn-theme-originally-set theme))
666 (context-coloring-apply-theme theme))))))
668 (defadvice enable-theme (after context-coloring-enable-theme (theme) activate)
669 "Enable colors for context themes just-in-time. We can't set
670 faces for custom themes that might not exist yet."
671 (when (and (not (eq theme 'user)) ; Called internally by `enable-theme'.
672 (custom-theme-p theme) ; Guard against non-existent themes.
673 (context-coloring-theme-p theme))
674 (context-coloring-enable-theme theme)))
676 (defadvice disable-theme (after context-coloring-disable-theme (theme) activate)
677 "Colors are disabled normally, but
678 `context-coloring-face-count' isn't. Update it here."
679 (when (custom-theme-p theme) ; Guard against non-existent themes.
680 (let ((enabled-theme (car custom-enabled-themes)))
681 (if (context-coloring-theme-p enabled-theme)
682 (context-coloring-enable-theme enabled-theme)
683 (context-coloring-set-colors-default)))))
685 (context-coloring-define-theme
697 (context-coloring-define-theme
712 (context-coloring-define-theme
725 (context-coloring-define-theme
738 (context-coloring-define-theme
741 :aliases '(solarized-light
743 sanityinc-solarized-light
744 sanityinc-solarized-dark)
763 (context-coloring-define-theme
775 (context-coloring-define-theme
792 (context-coloring-define-theme
811 (define-minor-mode context-coloring-mode
812 "Context-based code coloring, inspired by Douglas Crockford."
814 (if (not context-coloring-mode)
816 (context-coloring-kill-scopifier)
817 (when context-coloring-colorize-idle-timer
818 (cancel-timer context-coloring-colorize-idle-timer))
820 'js2-post-parse-callbacks 'context-coloring-colorize t)
822 'after-change-functions 'context-coloring-change-function t)
826 ;; Remember this buffer. This value should not be dynamically-bound.
827 (setq context-coloring-buffer (current-buffer))
829 ;; Font lock is incompatible with this mode; the converse is also true.
833 ;; Colorize once initially.
834 (context-coloring-colorize)
837 ((equal major-mode 'js2-mode)
838 ;; Only recolor on reparse.
839 (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t))
841 ;; Only recolor on change, idly.
842 (add-hook 'after-change-functions 'context-coloring-change-function nil t)
843 (setq context-coloring-colorize-idle-timer
845 context-coloring-delay
847 'context-coloring-maybe-colorize))))))
849 (provide 'context-coloring)
852 ;; eval: (when (fboundp 'rainbow-mode) (rainbow-mode 1))
855 ;;; context-coloring.el ends here