1 ;;; context-coloring.el --- Syntax highlighting, except not for syntax. -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2014 Jackson Ray Hamilton
5 ;; Author: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
6 ;; Keywords: context coloring syntax highlighting
8 ;; Package-Requires: ((emacs "24") (js2-mode "20141228"))
10 ;; This program is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
15 ;; This program is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
25 ;; Colors code by scope, rather than by syntax.
27 ;; A range of characters encompassing a scope is colored according to its level;
28 ;; the global scope is white, scopes within the global scope are yellow, scopes
29 ;; within scopes within the global scope are green, etc. Variables defined in a
30 ;; parent scope which are referenced from child scopes retain the same color as
31 ;; the scope in which they are defined; a variable defined in the global scope
32 ;; will be the same color when referenced from nested scopes.
34 ;; To use, add the following to your ~/.emacs:
36 ;; (require 'context-coloring)
37 ;; (add-hook 'js-mode-hook 'context-coloring-mode)
46 (defconst context-coloring-path
47 (file-name-directory (or load-file-name buffer-file-name))
48 "This file's directory.")
51 ;;; Customizable options
53 (defcustom context-coloring-delay 0.25
54 "Delay between a buffer update and colorization.
56 Increase this if your machine is high-performing. Decrease it if it ain't.
58 Supported modes: `js-mode', `js3-mode'"
59 :group 'context-coloring)
61 (defcustom context-coloring-js-block-scopes nil
62 "If non-nil, also color block scopes in the scope hierarchy in JavaScript.
64 The block-scope-inducing `let' and `const' are introduced in
65 ES6. If you are writing ES6 code, enable this; otherwise, don't.
67 Supported modes: `js2-mode'"
68 :group 'context-coloring)
70 (defcustom context-coloring-benchmark-colorization nil
71 "If non-nil, track how long colorization takes and print
72 messages with the colorization duration."
73 :group 'context-coloring)
78 (defvar-local context-coloring-buffer nil
79 "Reference to this buffer (for timers).")
81 (defvar-local context-coloring-scopifier-process nil
82 "Reference to the single scopifier process that can be
85 (defvar-local context-coloring-colorize-idle-timer nil
86 "Reference to the currently-running idle timer.")
88 (defvar-local context-coloring-changed nil
89 "Indication that the buffer has changed recently, which would
90 imply that it should be colorized again by
91 `context-coloring-colorize-idle-timer' if that timer is being
97 (defface context-coloring-level--1-face
98 '((((type tty)) (:foreground "white"))
99 (t (:foreground "#7f7f7f")))
100 "Context coloring face, level -1; comments."
101 :group 'context-coloring-faces)
103 (defface context-coloring-level-0-face
104 '((((type tty)) (:foreground "white"))
105 (((background light)) (:foreground "#000000"))
106 (((background dark)) (:foreground "#ffffff")))
107 "Context coloring face, level 0; global scope."
108 :group 'context-coloring-faces)
110 (defface context-coloring-level-1-face
111 '((((type tty)) (:foreground "yellow"))
112 (((background light)) (:foreground "#007f80"))
113 (((background dark)) (:foreground "#ffff80")))
114 "Context coloring face, level 1."
115 :group 'context-coloring-faces)
117 (defface context-coloring-level-2-face
118 '((((type tty)) (:foreground "green"))
119 (((background light)) (:foreground "#001580"))
120 (((background dark)) (:foreground "#cdfacd")))
121 "Context coloring face, level 2."
122 :group 'context-coloring-faces)
124 (defface context-coloring-level-3-face
125 '((((type tty)) (:foreground "cyan"))
126 (((background light)) (:foreground "#550080"))
127 (((background dark)) (:foreground "#d8d8ff")))
128 "Context coloring face, level 3."
129 :group 'context-coloring-faces)
131 (defface context-coloring-level-4-face
132 '((((type tty)) (:foreground "blue"))
133 (((background light)) (:foreground "#802b00"))
134 (((background dark)) (:foreground "#e7c7ff")))
135 "Context coloring face, level 4."
136 :group 'context-coloring-faces)
138 (defface context-coloring-level-5-face
139 '((((type tty)) (:foreground "magenta"))
140 (((background light)) (:foreground "#6a8000"))
141 (((background dark)) (:foreground "#ffcdcd")))
142 "Context coloring face, level 5."
143 :group 'context-coloring-faces)
145 (defface context-coloring-level-6-face
146 '((((type tty)) (:foreground "red"))
147 (((background light)) (:foreground "#008000"))
148 (((background dark)) (:foreground "#ffe390")))
149 "Context coloring face, level 6."
150 :group 'context-coloring-faces)
152 ;;; Additional 6 faces for insane levels of nesting
154 (defface context-coloring-level-7-face
155 '((t (:inherit context-coloring-level-1-face)))
156 "Context coloring face, level 7."
157 :group 'context-coloring-faces)
159 (defface context-coloring-level-8-face
160 '((t (:inherit context-coloring-level-2-face)))
161 "Context coloring face, level 8."
162 :group 'context-coloring-faces)
164 (defface context-coloring-level-9-face
165 '((t (:inherit context-coloring-level-3-face)))
166 "Context coloring face, level 9."
167 :group 'context-coloring-faces)
169 (defface context-coloring-level-10-face
170 '((t (:inherit context-coloring-level-4-face)))
171 "Context coloring face, level 10."
172 :group 'context-coloring-faces)
174 (defface context-coloring-level-11-face
175 '((t (:inherit context-coloring-level-5-face)))
176 "Context coloring face, level 11."
177 :group 'context-coloring-faces)
179 (defface context-coloring-level-12-face
180 '((t (:inherit context-coloring-level-6-face)))
181 "Context coloring face, level 12."
182 :group 'context-coloring-faces)
184 (defcustom context-coloring-face-count 7
185 "Number of faces defined for highlighting levels.
186 Determines level at which to cycle through faces again."
187 :group 'context-coloring)
192 (defsubst context-coloring-level-face (level)
193 "Return face-name for LEVEL as a string \"context-coloring-level-LEVEL-face\".
194 For example: \"context-coloring-level-1-face\"."
196 (concat "context-coloring-level-"
199 ;; Has a face directly mapping to it.
200 (and (< level context-coloring-face-count)
202 ;; After the number of available faces are used up, pretend the 0th
203 ;; face doesn't exist.
206 (- context-coloring-face-count 1)))))
210 ;;; Colorization utilities
212 (defun context-coloring-uncolorize-buffer ()
213 "Clears all coloring in the current buffer."
214 (remove-text-properties
217 `(face nil rear-nonsticky nil)))
219 (defsubst context-coloring-colorize-region (start end level)
220 "Colorizes characters from the 1-indexed START (inclusive) to
221 END (exclusive) with the face corresponding to LEVEL."
225 `(face ,(context-coloring-level-face level) rear-nonsticky t)))
228 ;;; js2-mode colorization
230 (defsubst context-coloring-js2-scope-level (scope)
231 "Gets the level of SCOPE."
235 (js2-node-parent scope)
236 (setq enclosing-scope (js2-node-get-enclosing-scope scope)))
237 (when (or context-coloring-js-block-scopes
238 (let ((type (js2-scope-type scope)))
239 (or (= type js2-SCRIPT)
240 (= type js2-FUNCTION)
241 (= type js2-CATCH))))
242 (setq level (+ level 1)))
243 (setq scope enclosing-scope))
246 ;; Adapted from js2-refactor.el/js2r-vars.el.
247 ;; FIXME: This fails if there is whitespace between the name and the colon.
248 (defsubst context-coloring-js2-local-name-node-p (node)
249 "Determines if NODE is a js2-name-node representing a local
251 (and (js2-name-node-p node)
252 (let ((start (js2-node-abs-pos node)))
254 (let ((end (+ start (js2-node-len node))))
255 (not (string-match "[\n\t ]*:" (buffer-substring-no-properties
258 (not (string-match "\\.[\n\t ]*" (buffer-substring-no-properties
259 (max 1 (- start 1)) ; 0 throws an
264 (defsubst context-coloring-js2-colorize-node (node level)
265 "Colors NODE with the color for LEVEL."
266 (let ((start (js2-node-abs-pos node)))
267 (context-coloring-colorize-region
269 (+ start (js2-node-len node)) ; End
272 (defun context-coloring-js2-colorize ()
273 "Colorizes the current buffer using the abstract syntax tree
274 generated by js2-mode."
275 (with-silent-modifications
281 ((js2-comment-node-p node)
282 (context-coloring-js2-colorize-node
286 (context-coloring-js2-colorize-node
288 (context-coloring-js2-scope-level node)))
289 ((context-coloring-js2-local-name-node-p node)
290 (context-coloring-js2-colorize-node
292 (context-coloring-js2-scope-level
293 (js2-get-defining-scope
294 (js2-node-get-enclosing-scope node)
295 (js2-name-node-name node))))))
296 ;; The `t' indicates to search children.
300 ;;; Shell command scopification / colorization
302 (defun context-coloring-apply-tokens (tokens)
303 "Processes a vector of TOKENS to apply context-based coloring
304 to the current buffer. Tokens are 3 integers: start, end,
305 level. The vector is flat, with a new token occurring after every
307 (with-silent-modifications
309 (len (length tokens)))
311 (context-coloring-colorize-region
314 (elt tokens (+ i 2)))
317 (defun context-coloring-parse-array (input)
318 "Specialized JSON parser for a flat array of numbers."
319 (vconcat (mapcar 'string-to-number (split-string (substring input 1 -1) ","))))
321 (defun context-coloring-kill-scopifier ()
322 "Kills the currently-running scopifier process for this
324 (when (not (null context-coloring-scopifier-process))
325 (delete-process context-coloring-scopifier-process)
326 (setq context-coloring-scopifier-process nil)))
328 (defun context-coloring-scopify-shell-command (command &optional callback)
329 "Invokes a scopifier with the current buffer's contents,
330 reading the scopifier's response asynchronously and applying a
331 parsed list of tokens to `context-coloring-apply-tokens'."
333 ;; Prior running tokenization is implicitly obsolete if this function is
335 (context-coloring-kill-scopifier)
337 ;; Start the process.
338 (setq context-coloring-scopifier-process
339 (start-process-shell-command "scopifier" nil command))
342 (buffer context-coloring-buffer))
344 ;; The process may produce output in multiple chunks. This filter
345 ;; accumulates the chunks into a message.
347 context-coloring-scopifier-process
348 (lambda (_process chunk)
349 (setq output (concat output chunk))))
351 ;; When the process's message is complete, this sentinel parses it as JSON
352 ;; and applies the tokens to the buffer.
353 (set-process-sentinel
354 context-coloring-scopifier-process
355 (lambda (_process event)
356 (when (equal "finished\n" event)
357 (let ((tokens (context-coloring-parse-array output)))
358 (with-current-buffer buffer
359 (context-coloring-apply-tokens tokens))
360 (setq context-coloring-scopifier-process nil)
361 (if callback (funcall callback)))))))
363 ;; Give the process its input so it can begin.
364 (process-send-region context-coloring-scopifier-process (point-min) (point-max))
365 (process-send-eof context-coloring-scopifier-process))
370 (defvar context-coloring-javascript-scopifier
371 `(:type shell-command
373 :command ,(expand-file-name
374 "./languages/javascript/bin/scopifier"
375 context-coloring-path)))
377 (defvar context-coloring-js2-colorizer
379 :colorizer context-coloring-js2-colorize))
381 (defcustom context-coloring-dispatch-plist
382 `(js-mode ,context-coloring-javascript-scopifier
383 js2-mode ,context-coloring-js2-colorizer
384 js3-mode ,context-coloring-javascript-scopifier)
385 "Property list mapping major modes to scopification programs."
386 :group 'context-coloring)
388 (defun context-coloring-dispatch (&optional callback)
389 "Determines the optimal track for scopification / colorization
390 of the current buffer, then does it."
391 (let ((dispatch (plist-get context-coloring-dispatch-plist major-mode)))
393 (message "%s" "Context coloring is not available for this major mode"))
394 (let ((type (plist-get dispatch :type)))
397 (let ((colorizer (plist-get dispatch :colorizer))
398 (scopifier (plist-get dispatch :scopifier)))
402 (if callback (funcall callback)))
404 (context-coloring-apply-tokens (funcall scopifier))
405 (if callback (funcall callback)))
407 (error "No `:colorizer' nor `:scopifier' specified for dispatch of `:type' elisp")))))
408 ((eq type 'shell-command)
409 (let ((executable (plist-get dispatch :executable))
410 (command (plist-get dispatch :command)))
412 (error "No `:command' specified for dispatch of `:type' shell-command"))
413 (if (and (not (null executable))
414 (null (executable-find executable)))
415 (message "Executable \"%s\" not found" executable))
416 (context-coloring-scopify-shell-command command callback)))))))
421 (defun context-coloring-colorize (&optional callback)
422 "Colors the current buffer by function context."
424 (let ((start-time (float-time)))
425 (context-coloring-dispatch
427 (when context-coloring-benchmark-colorization
428 (message "Colorization took %.3f seconds" (- (float-time) start-time)))
429 (if callback (funcall callback))))))
431 (defun context-coloring-change-function (_start _end _length)
432 "Registers a change so that a context-colored buffer can be
434 ;; Tokenization is obsolete if there was a change.
435 (context-coloring-kill-scopifier)
436 (setq context-coloring-changed t))
438 (defun context-coloring-maybe-colorize ()
439 "Colorize unders certain conditions. This will run as an idle
440 timer, so firstly the buffer must not be some other
441 buffer. Additionally, the buffer must have changed, otherwise
442 colorizing would be redundant."
443 (when (and (eq context-coloring-buffer (window-buffer (selected-window)))
444 context-coloring-changed)
445 (setq context-coloring-changed nil)
446 (context-coloring-colorize)))
452 (define-minor-mode context-coloring-mode
453 "Context-based code coloring, inspired by Douglas Crockford."
455 (if (not context-coloring-mode)
457 (context-coloring-kill-scopifier)
458 (when context-coloring-colorize-idle-timer
459 (cancel-timer context-coloring-colorize-idle-timer))
460 (remove-hook 'js2-post-parse-callbacks 'context-coloring-colorize t)
461 (remove-hook 'after-change-functions 'context-coloring-change-function t)
465 ;; Remember this buffer. This value should not be dynamically-bound.
466 (setq context-coloring-buffer (current-buffer))
468 ;; Font lock is incompatible with this mode; the converse is also true.
472 ;; Colorize once initially.
473 (context-coloring-colorize)
476 ((equal major-mode 'js2-mode)
477 ;; Only recolor on reparse.
478 (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t))
480 ;; Only recolor on change.
481 (add-hook 'after-change-functions 'context-coloring-change-function nil t)))
483 (when (not (equal major-mode 'js2-mode))
484 ;; Only recolor idly.
485 (setq context-coloring-colorize-idle-timer
486 (run-with-idle-timer context-coloring-delay t 'context-coloring-maybe-colorize)))))
488 (provide 'context-coloring)
490 ;;; context-coloring.el ends here