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)
149 (defsubst context-coloring-face-symbol (level)
150 "Returns a symbol for a face with LEVEL."
151 ;; `concat' is faster than `format' here.
152 (intern-soft (concat "context-coloring-level-"
153 (number-to-string level)
156 (defun context-coloring-set-colors (&rest colors)
157 "Set context coloring's levels' coloring to COLORS, where the
158 Nth element of COLORS is level N's color."
159 (setq context-coloring-face-count (length colors))
161 (dolist (color colors)
162 ;; Ensure there are available faces to contain new colors.
163 (when (not (context-coloring-face-symbol level))
164 (context-coloring-defface-default level))
165 (set-face-foreground (context-coloring-face-symbol level) color)
166 (setq level (+ level 1)))))
168 (defsubst context-coloring-level-face (level)
169 "Returns the face name for LEVEL."
170 (context-coloring-face-symbol (min level context-coloring-face-count)))
173 ;;; Colorization utilities
175 (defsubst context-coloring-colorize-region (start end level)
176 "Colorizes characters from the 1-indexed START (inclusive) to
177 END (exclusive) with the face corresponding to LEVEL."
181 `(face ,(context-coloring-level-face level))))
183 (defsubst context-coloring-maybe-colorize-comments-and-strings ()
184 "Colorizes the current buffer's comments and strings if
185 `context-coloring-comments-and-strings' is non-nil."
186 (when context-coloring-comments-and-strings
188 (font-lock-fontify-syntactically-region (point-min) (point-max)))))
191 ;;; js2-mode colorization
193 (defvar-local context-coloring-js2-scope-level-hash-table nil
194 "Associates `js2-scope' structures and with their scope
197 (defsubst context-coloring-js2-scope-level (scope)
198 "Gets the level of SCOPE."
199 (cond ((gethash scope context-coloring-js2-scope-level-hash-table))
202 (current-scope scope)
204 (while (and current-scope
205 (js2-node-parent current-scope)
206 (setq enclosing-scope
207 (js2-node-get-enclosing-scope current-scope)))
208 (when (or context-coloring-js-block-scopes
209 (let ((type (js2-scope-type current-scope)))
210 (or (= type js2-SCRIPT)
211 (= type js2-FUNCTION)
212 (= type js2-CATCH))))
213 (setq level (+ level 1)))
214 (setq current-scope enclosing-scope))
215 (puthash scope level context-coloring-js2-scope-level-hash-table)))))
217 (defsubst context-coloring-js2-local-name-node-p (node)
218 "Determines if NODE is a js2-name-node representing a local
220 (and (js2-name-node-p node)
221 (let ((parent (js2-node-parent node)))
222 (not (or (and (js2-object-prop-node-p parent)
223 (eq node (js2-object-prop-node-left parent)))
224 (and (js2-prop-get-node-p parent)
225 ;; For nested property lookup, the node on the left is a
226 ;; `js2-prop-get-node', so this always works.
227 (eq node (js2-prop-get-node-right parent))))))))
229 (defsubst context-coloring-js2-colorize-node (node level)
230 "Colors NODE with the color for LEVEL."
231 (let ((start (js2-node-abs-pos node)))
232 (context-coloring-colorize-region
234 (+ start (js2-node-len node)) ; End
237 (defun context-coloring-js2-colorize ()
238 "Colorizes the current buffer using the abstract syntax tree
239 generated by js2-mode."
240 ;; Reset the hash table; the old one could be obsolete.
241 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test 'eq))
242 (with-silent-modifications
249 (context-coloring-js2-colorize-node
251 (context-coloring-js2-scope-level node)))
252 ((context-coloring-js2-local-name-node-p node)
253 (let* ((enclosing-scope (js2-node-get-enclosing-scope node))
254 (defining-scope (js2-get-defining-scope
256 (js2-name-node-name node))))
257 ;; The tree seems to be walked lexically, so an entire scope will
258 ;; be colored, including its name nodes, before they are reached.
259 ;; Coloring the nodes defined in that scope would be redundant, so
261 (when (not (eq defining-scope enclosing-scope))
262 (context-coloring-js2-colorize-node
264 (context-coloring-js2-scope-level defining-scope))))))
265 ;; The `t' indicates to search children.
267 (context-coloring-maybe-colorize-comments-and-strings)))
270 ;;; Shell command scopification / colorization
272 (defun context-coloring-apply-tokens (tokens)
273 "Processes a vector of TOKENS to apply context-based coloring
274 to the current buffer. Tokens are 3 integers: start, end, level.
275 The vector is flat, with a new token occurring after every 3rd
277 (with-silent-modifications
279 (len (length tokens)))
281 (context-coloring-colorize-region
284 (elt tokens (+ i 2)))
286 (context-coloring-maybe-colorize-comments-and-strings)))
288 (defun context-coloring-parse-array (input)
289 "Specialized JSON parser for a flat array of numbers."
290 (vconcat (mapcar 'string-to-number (split-string (substring input 1 -1) ","))))
292 (defun context-coloring-kill-scopifier ()
293 "Kills the currently-running scopifier process for this
295 (when (not (null context-coloring-scopifier-process))
296 (delete-process context-coloring-scopifier-process)
297 (setq context-coloring-scopifier-process nil)))
299 (defun context-coloring-scopify-shell-command (command &optional callback)
300 "Invokes a scopifier with the current buffer's contents,
301 reading the scopifier's response asynchronously and applying a
302 parsed list of tokens to `context-coloring-apply-tokens'.
304 Invokes CALLBACK when complete."
306 ;; Prior running tokenization is implicitly obsolete if this function is
308 (context-coloring-kill-scopifier)
310 ;; Start the process.
311 (setq context-coloring-scopifier-process
312 (start-process-shell-command "scopifier" nil command))
315 (buffer context-coloring-buffer))
317 ;; The process may produce output in multiple chunks. This filter
318 ;; accumulates the chunks into a message.
320 context-coloring-scopifier-process
321 (lambda (_process chunk)
322 (setq output (concat output chunk))))
324 ;; When the process's message is complete, this sentinel parses it as JSON
325 ;; and applies the tokens to the buffer.
326 (set-process-sentinel
327 context-coloring-scopifier-process
328 (lambda (_process event)
329 (when (equal "finished\n" event)
330 (let ((tokens (context-coloring-parse-array output)))
331 (with-current-buffer buffer
332 (context-coloring-apply-tokens tokens))
333 (setq context-coloring-scopifier-process nil)
334 (if callback (funcall callback)))))))
336 ;; Give the process its input so it can begin.
337 (process-send-region context-coloring-scopifier-process (point-min) (point-max))
338 (process-send-eof context-coloring-scopifier-process))
343 (defvar context-coloring-dispatch-hash-table (make-hash-table :test 'eq)
344 "Mapping of dispatch strategy names to their corresponding
345 property lists, which contain details about the strategies.")
347 (defvar context-coloring-mode-hash-table (make-hash-table :test 'eq)
348 "Mapping of major mode names to dispatch property lists.")
350 (defun context-coloring-select-dispatch (mode dispatch)
351 "Use DISPATCH for MODE."
356 context-coloring-dispatch-hash-table)
357 context-coloring-mode-hash-table))
359 (defun context-coloring-define-dispatch (symbol &rest properties)
360 "Define a new dispatch named SYMBOL with PROPERTIES.
362 A \"dispatch\" is a property list describing a strategy for
363 coloring a buffer. There are three possible strategies: Parse
364 and color in a single function (`:colorizer'), parse in a
365 function that returns scope data (`:scopifier'), or parse with a
366 shell command that returns scope data (`:command'). In the
367 latter two cases, the scope data will be used to automatically
370 PROPERTIES must include `:modes' and one of `:colorizer',
371 `:scopifier' or `:command'.
373 `:modes' - List of major modes this dispatch is valid for.
375 `:colorizer' - Symbol referring to a function that parses and
378 `:scopifier' - Symbol referring to a function that parses the
379 buffer a returns a flat vector of start, end and level data.
381 `:executable' - Optional name of an executable required by
384 `:command' - Shell command to execute with the current buffer
385 sent via stdin, and with a flat JSON array of start, end and
386 level data returned via stdout."
387 (let ((modes (plist-get properties :modes))
388 (colorizer (plist-get properties :colorizer))
389 (scopifier (plist-get properties :scopifier))
390 (command (plist-get properties :command)))
392 (error "No mode defined for dispatch"))
393 (when (not (or colorizer
396 (error "No colorizer, scopifier or command defined for dispatch"))
397 (puthash symbol properties context-coloring-dispatch-hash-table)
399 (when (null (gethash mode context-coloring-mode-hash-table))
400 (puthash mode properties context-coloring-mode-hash-table)))))
402 (context-coloring-define-dispatch
404 :modes '(js-mode js3-mode)
405 :executable "scopifier"
406 :command "scopifier")
408 (context-coloring-define-dispatch
411 :colorizer 'context-coloring-js2-colorize)
413 (defun context-coloring-dispatch (&optional callback)
414 "Determines the optimal track for scopification / colorization
415 of the current buffer, then executes it.
417 Invokes CALLBACK when complete. It is invoked synchronously for
418 elisp tracks, and asynchronously for shell command tracks."
419 (let ((dispatch (gethash major-mode context-coloring-mode-hash-table)))
421 (message "%s" "Context coloring is not available for this major mode"))
427 ((setq colorizer (plist-get dispatch :colorizer))
429 (if callback (funcall callback)))
430 ((setq scopifier (plist-get dispatch :scopifier))
431 (context-coloring-apply-tokens (funcall scopifier))
432 (if callback (funcall callback)))
433 ((setq command (plist-get dispatch :command))
434 (setq executable (plist-get dispatch :executable))
436 (null (executable-find executable)))
437 (message "Executable \"%s\" not found" executable)
438 (context-coloring-scopify-shell-command command callback)))))))
443 (defun context-coloring-colorize (&optional callback)
444 "Colors the current buffer by function context.
446 Invokes CALLBACK when complete; see `context-coloring-dispatch'."
448 (let ((start-time (float-time)))
449 (context-coloring-dispatch
451 (when context-coloring-benchmark-colorization
452 (message "Colorization took %.3f seconds" (- (float-time) start-time)))
453 (if callback (funcall callback))))))
455 (defun context-coloring-change-function (_start _end _length)
456 "Registers a change so that a buffer can be colorized soon."
457 ;; Tokenization is obsolete if there was a change.
458 (context-coloring-kill-scopifier)
459 (setq context-coloring-changed t))
461 (defun context-coloring-maybe-colorize ()
462 "Colorize unders certain conditions. This will run as an idle
463 timer, so firstly the buffer must not be some other buffer.
464 Additionally, the buffer must have changed, otherwise colorizing
466 (when (and (eq context-coloring-buffer (window-buffer (selected-window)))
467 context-coloring-changed)
468 (setq context-coloring-changed nil)
469 (context-coloring-colorize)))
474 (defvar context-coloring-theme-hash-table (make-hash-table :test 'eq)
475 "Mapping of theme names to theme properties.")
477 (defun context-coloring-define-theme (theme &rest properties)
478 "Define a theme named THEME for coloring scope levels.
479 PROPERTIES is a property list specifiying the following details:
481 `:colors': List of colors that this theme uses."
485 (apply 'context-coloring-set-colors (plist-get properties :colors)))
486 context-coloring-theme-hash-table))
488 (defun context-coloring-load-theme (theme)
489 "Apply THEME's colors and other properties for context
491 (let ((function (gethash theme context-coloring-theme-hash-table)))
492 (when (null function)
493 (error (format "No such theme `%s'" theme)))
496 (context-coloring-define-theme
508 (context-coloring-define-theme
528 (context-coloring-define-theme
544 (context-coloring-define-theme
562 (define-minor-mode context-coloring-mode
563 "Context-based code coloring, inspired by Douglas Crockford."
565 (if (not context-coloring-mode)
567 (context-coloring-kill-scopifier)
568 (when context-coloring-colorize-idle-timer
569 (cancel-timer context-coloring-colorize-idle-timer))
570 (remove-hook 'js2-post-parse-callbacks 'context-coloring-colorize t)
571 (remove-hook 'after-change-functions 'context-coloring-change-function t)
575 ;; Remember this buffer. This value should not be dynamically-bound.
576 (setq context-coloring-buffer (current-buffer))
578 ;; Font lock is incompatible with this mode; the converse is also true.
582 ;; Colorize once initially.
583 (context-coloring-colorize)
586 ((equal major-mode 'js2-mode)
587 ;; Only recolor on reparse.
588 (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t))
590 ;; Only recolor on change.
591 (add-hook 'after-change-functions 'context-coloring-change-function nil t)))
593 (when (not (equal major-mode 'js2-mode))
594 ;; Only recolor idly.
595 (setq context-coloring-colorize-idle-timer
597 context-coloring-delay
599 'context-coloring-maybe-colorize)))))
601 (provide 'context-coloring)
604 ;; eval: (when (fboundp 'rainbow-mode) (rainbow-mode 1))
607 ;;; context-coloring.el ends here