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
61 (defconst context-coloring-path
62 (file-name-directory (or load-file-name buffer-file-name))
63 "This file's directory.")
66 ;;; Customizable options
68 (defcustom context-coloring-delay 0.25
69 "Delay between a buffer update and colorization.
71 Increase this if your machine is high-performing. Decrease it if
74 Supported modes: `js-mode', `js3-mode'"
75 :group 'context-coloring)
77 (defcustom context-coloring-comments-and-strings t
78 "If non-nil, also color comments and strings using `font-lock'."
79 :group 'context-coloring)
81 (defcustom context-coloring-js-block-scopes nil
82 "If non-nil, also color block scopes in the scope hierarchy in JavaScript.
84 The block-scope-inducing `let' and `const' are introduced in ES6.
85 If you are writing ES6 code, enable this; otherwise, don't.
87 Supported modes: `js2-mode'"
88 :group 'context-coloring)
90 (defcustom context-coloring-benchmark-colorization nil
91 "If non-nil, track how long colorization takes and print
92 messages with the colorization duration."
93 :group 'context-coloring)
98 (defvar-local context-coloring-buffer nil
99 "Reference to this buffer (for timers).")
101 (defvar-local context-coloring-scopifier-process nil
102 "Reference to the single scopifier process that can be
105 (defvar-local context-coloring-colorize-idle-timer nil
106 "Reference to the currently-running idle timer.")
108 (defvar-local context-coloring-changed nil
109 "Indication that the buffer has changed recently, which would
110 imply that it should be colorized again by
111 `context-coloring-colorize-idle-timer' if that timer is being
117 (defun context-coloring-defface (level tty light dark)
118 (let ((face (intern (format "context-coloring-level-%s-face" level)))
119 (doc (format "Context coloring face, level %s." level)))
120 (eval (macroexpand `(defface ,face
121 '((((type tty)) (:foreground ,tty))
122 (((background light)) (:foreground ,light))
123 (((background dark)) (:foreground ,dark)))
125 :group 'context-coloring)))))
127 (defvar context-coloring-face-count nil
128 "Number of faces available for context coloring.")
130 (defun context-coloring-defface-default (level)
131 (context-coloring-defface level "white" "#3f3f3f" "#cdcdcd"))
133 (defun context-coloring-set-colors-default ()
134 (context-coloring-defface 0 "white" "#000000" "#ffffff")
135 (context-coloring-defface 1 "yellow" "#007f80" "#ffff80")
136 (context-coloring-defface 2 "green" "#001580" "#cdfacd")
137 (context-coloring-defface 3 "cyan" "#550080" "#d8d8ff")
138 (context-coloring-defface 4 "blue" "#802b00" "#e7c7ff")
139 (context-coloring-defface 5 "magenta" "#6a8000" "#ffcdcd")
140 (context-coloring-defface 6 "red" "#008000" "#ffe390")
141 (context-coloring-defface-default 7)
142 (setq context-coloring-face-count 8))
144 (context-coloring-set-colors-default)
146 ;; Color theme authors can have up to 26 levels: 1 (0th) for globals, 24
147 ;; (1st-24th) for in-betweens, and 1 (25th) for infinity.
149 (context-coloring-defface-default (+ number context-coloring-face-count)))
154 (defsubst context-coloring-face-symbol (level)
155 "Returns a symbol for a face with LEVEL."
156 ;; `concat' is faster than `format' here.
157 (intern-soft (concat "context-coloring-level-"
158 (number-to-string level)
161 (defun context-coloring-set-colors (&rest colors)
162 "Set context coloring's levels' coloring to COLORS, where the
163 Nth element of COLORS is level N's color."
164 (setq context-coloring-face-count (length colors))
166 (dolist (color colors)
167 ;; Ensure there are available faces to contain new colors.
168 (when (not (context-coloring-face-symbol level))
169 (context-coloring-defface-default level))
170 (set-face-foreground (context-coloring-face-symbol level) color)
171 (setq level (+ level 1)))))
173 (defsubst context-coloring-level-face (level)
174 "Returns the face name for LEVEL."
175 (context-coloring-face-symbol (min level context-coloring-face-count)))
178 ;;; Colorization utilities
180 (defsubst context-coloring-colorize-region (start end level)
181 "Colorizes characters from the 1-indexed START (inclusive) to
182 END (exclusive) with the face corresponding to LEVEL."
186 `(face ,(context-coloring-level-face level))))
188 (defsubst context-coloring-maybe-colorize-comments-and-strings ()
189 "Colorizes the current buffer's comments and strings if
190 `context-coloring-comments-and-strings' is non-nil."
191 (when context-coloring-comments-and-strings
193 (font-lock-fontify-syntactically-region (point-min) (point-max)))))
196 ;;; js2-mode colorization
198 (defvar-local context-coloring-js2-scope-level-hash-table nil
199 "Associates `js2-scope' structures and with their scope
202 (defsubst context-coloring-js2-scope-level (scope)
203 "Gets the level of SCOPE."
204 (cond ((gethash scope context-coloring-js2-scope-level-hash-table))
207 (current-scope scope)
209 (while (and current-scope
210 (js2-node-parent current-scope)
211 (setq enclosing-scope
212 (js2-node-get-enclosing-scope current-scope)))
213 (when (or context-coloring-js-block-scopes
214 (let ((type (js2-scope-type current-scope)))
215 (or (= type js2-SCRIPT)
216 (= type js2-FUNCTION)
217 (= type js2-CATCH))))
218 (setq level (+ level 1)))
219 (setq current-scope enclosing-scope))
220 (puthash scope level context-coloring-js2-scope-level-hash-table)))))
222 (defsubst context-coloring-js2-local-name-node-p (node)
223 "Determines if NODE is a js2-name-node representing a local
225 (and (js2-name-node-p node)
226 (let ((parent (js2-node-parent node)))
227 (not (or (and (js2-object-prop-node-p parent)
228 (eq node (js2-object-prop-node-left parent)))
229 (and (js2-prop-get-node-p parent)
230 ;; For nested property lookup, the node on the left is a
231 ;; `js2-prop-get-node', so this always works.
232 (eq node (js2-prop-get-node-right parent))))))))
234 (defsubst context-coloring-js2-colorize-node (node level)
235 "Colors NODE with the color for LEVEL."
236 (let ((start (js2-node-abs-pos node)))
237 (context-coloring-colorize-region
239 (+ start (js2-node-len node)) ; End
242 (defun context-coloring-js2-colorize ()
243 "Colorizes the current buffer using the abstract syntax tree
244 generated by js2-mode."
245 ;; Reset the hash table; the old one could be obsolete.
246 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test 'eq))
247 (with-silent-modifications
254 (context-coloring-js2-colorize-node
256 (context-coloring-js2-scope-level node)))
257 ((context-coloring-js2-local-name-node-p node)
258 (let* ((enclosing-scope (js2-node-get-enclosing-scope node))
259 (defining-scope (js2-get-defining-scope
261 (js2-name-node-name node))))
262 ;; The tree seems to be walked lexically, so an entire scope will
263 ;; be colored, including its name nodes, before they are reached.
264 ;; Coloring the nodes defined in that scope would be redundant, so
266 (when (not (eq defining-scope enclosing-scope))
267 (context-coloring-js2-colorize-node
269 (context-coloring-js2-scope-level defining-scope))))))
270 ;; The `t' indicates to search children.
272 (context-coloring-maybe-colorize-comments-and-strings)))
275 ;;; Shell command scopification / colorization
277 (defun context-coloring-apply-tokens (tokens)
278 "Processes a vector of TOKENS to apply context-based coloring
279 to the current buffer. Tokens are 3 integers: start, end, level.
280 The vector is flat, with a new token occurring after every 3rd
282 (with-silent-modifications
284 (len (length tokens)))
286 (context-coloring-colorize-region
289 (elt tokens (+ i 2)))
291 (context-coloring-maybe-colorize-comments-and-strings)))
293 (defun context-coloring-parse-array (input)
294 "Specialized JSON parser for a flat array of numbers."
295 (vconcat (mapcar 'string-to-number (split-string (substring input 1 -1) ","))))
297 (defun context-coloring-kill-scopifier ()
298 "Kills the currently-running scopifier process for this
300 (when (not (null context-coloring-scopifier-process))
301 (delete-process context-coloring-scopifier-process)
302 (setq context-coloring-scopifier-process nil)))
304 (defun context-coloring-scopify-shell-command (command &optional callback)
305 "Invokes a scopifier with the current buffer's contents,
306 reading the scopifier's response asynchronously and applying a
307 parsed list of tokens to `context-coloring-apply-tokens'.
309 Invokes CALLBACK when complete."
311 ;; Prior running tokenization is implicitly obsolete if this function is
313 (context-coloring-kill-scopifier)
315 ;; Start the process.
316 (setq context-coloring-scopifier-process
317 (start-process-shell-command "scopifier" nil command))
320 (buffer context-coloring-buffer))
322 ;; The process may produce output in multiple chunks. This filter
323 ;; accumulates the chunks into a message.
325 context-coloring-scopifier-process
326 (lambda (_process chunk)
327 (setq output (concat output chunk))))
329 ;; When the process's message is complete, this sentinel parses it as JSON
330 ;; and applies the tokens to the buffer.
331 (set-process-sentinel
332 context-coloring-scopifier-process
333 (lambda (_process event)
334 (when (equal "finished\n" event)
335 (let ((tokens (context-coloring-parse-array output)))
336 (with-current-buffer buffer
337 (context-coloring-apply-tokens tokens))
338 (setq context-coloring-scopifier-process nil)
339 (if callback (funcall callback)))))))
341 ;; Give the process its input so it can begin.
342 (process-send-region context-coloring-scopifier-process (point-min) (point-max))
343 (process-send-eof context-coloring-scopifier-process))
348 (defvar context-coloring-dispatch-hash-table (make-hash-table :test 'eq)
349 "Mapping of dispatch strategy names to their corresponding
350 property lists, which contain details about the strategies.")
352 (defvar context-coloring-mode-hash-table (make-hash-table :test 'eq)
353 "Mapping of major mode names to dispatch property lists.")
355 (defun context-coloring-select-dispatch (mode dispatch)
356 "Use DISPATCH for MODE."
361 context-coloring-dispatch-hash-table)
362 context-coloring-mode-hash-table))
364 (defun context-coloring-define-dispatch (symbol &rest properties)
365 "Define a new dispatch named SYMBOL with PROPERTIES.
367 A \"dispatch\" is a property list describing a strategy for
368 coloring a buffer. There are three possible strategies: Parse
369 and color in a single function (`:colorizer'), parse in a
370 function that returns scope data (`:scopifier'), or parse with a
371 shell command that returns scope data (`:command'). In the
372 latter two cases, the scope data will be used to automatically
375 PROPERTIES must include `:modes' and one of `:colorizer',
376 `:scopifier' or `:command'.
378 `:modes' - List of major modes this dispatch is valid for.
380 `:colorizer' - Symbol referring to a function that parses and
383 `:scopifier' - Symbol referring to a function that parses the
384 buffer a returns a flat vector of start, end and level data.
386 `:executable' - Optional name of an executable required by
389 `:command' - Shell command to execute with the current buffer
390 sent via stdin, and with a flat JSON array of start, end and
391 level data returned via stdout."
392 (let ((modes (plist-get properties :modes))
393 (colorizer (plist-get properties :colorizer))
394 (scopifier (plist-get properties :scopifier))
395 (command (plist-get properties :command)))
397 (error "No mode defined for dispatch"))
398 (when (not (or colorizer
401 (error "No colorizer, scopifier or command defined for dispatch"))
402 (puthash symbol properties context-coloring-dispatch-hash-table)
404 (when (null (gethash mode context-coloring-mode-hash-table))
405 (puthash mode properties context-coloring-mode-hash-table)))))
407 (context-coloring-define-dispatch
409 :modes '(js-mode js3-mode)
410 :executable "scopifier"
411 :command "scopifier")
413 (context-coloring-define-dispatch
416 :colorizer 'context-coloring-js2-colorize)
418 (defun context-coloring-dispatch (&optional callback)
419 "Determines the optimal track for scopification / colorization
420 of the current buffer, then executes it.
422 Invokes CALLBACK when complete. It is invoked synchronously for
423 elisp tracks, and asynchronously for shell command tracks."
424 (let ((dispatch (gethash major-mode context-coloring-mode-hash-table)))
426 (message "%s" "Context coloring is not available for this major mode"))
432 ((setq colorizer (plist-get dispatch :colorizer))
434 (if callback (funcall callback)))
435 ((setq scopifier (plist-get dispatch :scopifier))
436 (context-coloring-apply-tokens (funcall scopifier))
437 (if callback (funcall callback)))
438 ((setq command (plist-get dispatch :command))
439 (setq executable (plist-get dispatch :executable))
441 (null (executable-find executable)))
442 (message "Executable \"%s\" not found" executable)
443 (context-coloring-scopify-shell-command command callback)))))))
448 (defun context-coloring-colorize (&optional callback)
449 "Colors the current buffer by function context.
451 Invokes CALLBACK when complete; see `context-coloring-dispatch'."
453 (let ((start-time (float-time)))
454 (context-coloring-dispatch
456 (when context-coloring-benchmark-colorization
457 (message "Colorization took %.3f seconds" (- (float-time) start-time)))
458 (if callback (funcall callback))))))
460 (defun context-coloring-change-function (_start _end _length)
461 "Registers a change so that a buffer can be colorized soon."
462 ;; Tokenization is obsolete if there was a change.
463 (context-coloring-kill-scopifier)
464 (setq context-coloring-changed t))
466 (defun context-coloring-maybe-colorize ()
467 "Colorize unders certain conditions. This will run as an idle
468 timer, so firstly the buffer must not be some other buffer.
469 Additionally, the buffer must have changed, otherwise colorizing
471 (when (and (eq context-coloring-buffer (window-buffer (selected-window)))
472 context-coloring-changed)
473 (setq context-coloring-changed nil)
474 (context-coloring-colorize)))
479 (defvar context-coloring-theme-hash-table (make-hash-table :test 'eq)
480 "Mapping of theme names to theme properties.")
482 (defun context-coloring-apply-theme (theme)
483 "Applies THEME's properties to its respective custom theme,
484 which must already exist and which *should* already be enabled."
485 (let ((properties (gethash theme context-coloring-theme-hash-table)))
486 (when (null properties)
487 (error (format "No such theme `%s'" theme)))
488 (let ((colors (plist-get properties :colors)))
489 (setq context-coloring-face-count (length colors)) ; Side-effect?
491 ;; AFAIK, no way to know if a theme already has a face set, so just
492 ;; override blindly for now.
494 'custom-theme-set-faces
498 (setq level (+ level 1))
499 `(,(context-coloring-face-symbol level) ((t (:foreground ,color)))))
502 (defun context-coloring-define-theme (theme &rest properties)
503 "Define a theme named THEME for coloring scope levels.
504 PROPERTIES is a property list specifiying the following details:
506 `:colors': List of colors that this theme uses."
507 (let ((aliases (plist-get properties :aliases)))
508 (dolist (name (append `(,theme) aliases))
509 (puthash name properties context-coloring-theme-hash-table)
510 ;; Compensate for already-enabled themes by applying their colors now.
511 (when (custom-theme-enabled-p name)
512 (context-coloring-apply-theme name)))))
514 (defun context-coloring-load-theme (&optional rest)
516 "themes are now loaded alongside custom themes automatically."
519 (defadvice enable-theme (after context-coloring-enable-theme (theme) activate)
520 "Add colors to themes just-in-time."
521 (when (and (not (eq theme 'user)) ; Called internally.
522 (custom-theme-p theme)) ; Guard against non-existent themes.
523 (context-coloring-apply-theme theme)))
525 (context-coloring-define-theme
537 (context-coloring-define-theme
549 (context-coloring-define-theme
551 :aliases '(solarized-light
553 sanityinc-solarized-light
554 sanityinc-solarized-dark)
573 (context-coloring-define-theme
589 (context-coloring-define-theme
607 (define-minor-mode context-coloring-mode
608 "Context-based code coloring, inspired by Douglas Crockford."
610 (if (not context-coloring-mode)
612 (context-coloring-kill-scopifier)
613 (when context-coloring-colorize-idle-timer
614 (cancel-timer context-coloring-colorize-idle-timer))
615 (remove-hook 'js2-post-parse-callbacks 'context-coloring-colorize t)
616 (remove-hook 'after-change-functions 'context-coloring-change-function t)
620 ;; Remember this buffer. This value should not be dynamically-bound.
621 (setq context-coloring-buffer (current-buffer))
623 ;; Font lock is incompatible with this mode; the converse is also true.
627 ;; Colorize once initially.
628 (context-coloring-colorize)
631 ((equal major-mode 'js2-mode)
632 ;; Only recolor on reparse.
633 (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t))
635 ;; Only recolor on change.
636 (add-hook 'after-change-functions 'context-coloring-change-function nil t)))
638 (when (not (equal major-mode 'js2-mode))
639 ;; Only recolor idly.
640 (setq context-coloring-colorize-idle-timer
642 context-coloring-delay
644 'context-coloring-maybe-colorize)))))
646 (provide 'context-coloring)
649 ;; eval: (when (fboundp 'rainbow-mode) (rainbow-mode 1))
652 ;;; context-coloring.el ends here