]> code.delx.au - gnu-emacs-elpa/blob - context-coloring.el
Merge branch 'master' into develop
[gnu-emacs-elpa] / context-coloring.el
1 ;;; context-coloring.el --- Syntax highlighting, except not for syntax. -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2014 Jackson Ray Hamilton
4
5 ;; Author: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
6 ;; Keywords: context coloring syntax highlighting
7 ;; Version: 2.0.1
8 ;; Package-Requires: ((emacs "24") (js2-mode "20141228"))
9
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.
14
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.
19
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/>.
22
23 ;;; Commentary:
24
25 ;; Colors code by scope, rather than by syntax.
26
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.
33
34 ;; To use, add the following to your ~/.emacs:
35
36 ;; (require 'context-coloring)
37 ;; (add-hook 'js-mode-hook 'context-coloring-mode)
38
39 ;;; Code:
40
41 (require 'js2-mode)
42
43
44 ;;; Constants
45
46 (defconst context-coloring-path
47 (file-name-directory (or load-file-name buffer-file-name))
48 "This file's directory.")
49
50
51 ;;; Customizable options
52
53 (defcustom context-coloring-delay 0.25
54 "Delay between a buffer update and colorization.
55
56 Increase this if your machine is high-performing. Decrease it if it ain't.
57
58 Supported modes: `js-mode', `js3-mode'"
59 :group 'context-coloring)
60
61 (defcustom context-coloring-js-block-scopes nil
62 "If non-nil, also color block scopes in the scope hierarchy in JavaScript.
63
64 The block-scope-inducing `let' and `const' are introduced in
65 ES6. If you are writing ES6 code, enable this; otherwise, don't.
66
67 Supported modes: `js2-mode'"
68 :group 'context-coloring)
69
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)
74
75
76 ;;; Local variables
77
78 (defvar-local context-coloring-buffer nil
79 "Reference to this buffer (for timers).")
80
81 (defvar-local context-coloring-scopifier-process nil
82 "Reference to the single scopifier process that can be
83 running.")
84
85 (defvar-local context-coloring-colorize-idle-timer nil
86 "Reference to the currently-running idle timer.")
87
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
92 used.")
93
94
95 ;;; Faces
96
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)
102
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)
109
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)
116
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)
123
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)
130
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)
137
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)
144
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)
151
152 ;;; Additional 6 faces for insane levels of nesting
153
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)
158
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)
163
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)
168
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)
173
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)
178
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)
183
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)
188
189
190 ;;; Face functions
191
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\"."
195 (intern-soft
196 (concat "context-coloring-level-"
197 (number-to-string
198 (or
199 ;; Has a face directly mapping to it.
200 (and (< level context-coloring-face-count)
201 level)
202 ;; After the number of available faces are used up, pretend the 0th
203 ;; face doesn't exist.
204 (+ 1
205 (mod (- level 1)
206 (- context-coloring-face-count 1)))))
207 "-face")))
208
209
210 ;;; Colorization utilities
211
212 (defun context-coloring-uncolorize-buffer ()
213 "Clears all coloring in the current buffer."
214 (remove-text-properties
215 (point-min)
216 (point-max)
217 `(face nil rear-nonsticky nil)))
218
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."
222 (add-text-properties
223 start
224 end
225 `(face ,(context-coloring-level-face level) rear-nonsticky t)))
226
227
228 ;;; js2-mode colorization
229
230 (defvar-local context-coloring-js2-scope-level-hash-table nil
231 "Associates `js2-scope' structures and with their scope
232 levels.")
233
234 (defsubst context-coloring-js2-scope-level (scope)
235 "Gets the level of SCOPE."
236 (cond ((gethash scope context-coloring-js2-scope-level-hash-table))
237 (t
238 (let ((level 0)
239 (current-scope scope)
240 enclosing-scope)
241 (while (and current-scope
242 (js2-node-parent current-scope)
243 (setq enclosing-scope (js2-node-get-enclosing-scope current-scope)))
244 (when (or context-coloring-js-block-scopes
245 (let ((type (js2-scope-type current-scope)))
246 (or (= type js2-SCRIPT)
247 (= type js2-FUNCTION)
248 (= type js2-CATCH))))
249 (setq level (+ level 1)))
250 (setq current-scope enclosing-scope))
251 (puthash scope level context-coloring-js2-scope-level-hash-table)))))
252
253 (defsubst context-coloring-js2-local-name-node-p (node)
254 "Determines if NODE is a js2-name-node representing a local
255 variable."
256 (and (js2-name-node-p node)
257 (let ((parent (js2-node-parent node)))
258 (not (or (and (js2-object-prop-node-p parent)
259 (eq node (js2-object-prop-node-left parent)))
260 (and (js2-prop-get-node-p parent)
261 ;; For nested property lookup, the node on the left is a
262 ;; `js2-prop-get-node', so this always works.
263 (eq node (js2-prop-get-node-right parent))))))))
264
265 (defsubst context-coloring-js2-colorize-node (node level)
266 "Colors NODE with the color for LEVEL."
267 (let ((start (js2-node-abs-pos node)))
268 (context-coloring-colorize-region
269 start
270 (+ start (js2-node-len node)) ; End
271 level)))
272
273 (defun context-coloring-js2-colorize ()
274 "Colorizes the current buffer using the abstract syntax tree
275 generated by js2-mode."
276 ;; Reset the hash table; the old one could be obsolete.
277 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test 'eq))
278 (with-silent-modifications
279 (js2-visit-ast
280 js2-mode-ast
281 (lambda (node end-p)
282 (when (null end-p)
283 (cond
284 ((js2-comment-node-p node)
285 (context-coloring-js2-colorize-node
286 node
287 -1))
288 ((js2-scope-p node)
289 (context-coloring-js2-colorize-node
290 node
291 (context-coloring-js2-scope-level node)))
292 ((context-coloring-js2-local-name-node-p node)
293 (let* ((enclosing-scope (js2-node-get-enclosing-scope node))
294 (defining-scope (js2-get-defining-scope
295 enclosing-scope
296 (js2-name-node-name node))))
297 ;; The tree seems to be walked lexically, so an entire scope will
298 ;; be colored, including its name nodes, before they are
299 ;; reached. Coloring the nodes defined in that scope would be
300 ;; redundant, so don't do it.
301 (when (not (eq defining-scope enclosing-scope))
302 (context-coloring-js2-colorize-node
303 node
304 (context-coloring-js2-scope-level defining-scope))))))
305 ;; The `t' indicates to search children.
306 t)))))
307
308
309 ;;; Shell command scopification / colorization
310
311 (defun context-coloring-apply-tokens (tokens)
312 "Processes a vector of TOKENS to apply context-based coloring
313 to the current buffer. Tokens are 3 integers: start, end,
314 level. The vector is flat, with a new token occurring after every
315 3rd element."
316 (with-silent-modifications
317 (let ((i 0)
318 (len (length tokens)))
319 (while (< i len)
320 (context-coloring-colorize-region
321 (elt tokens i)
322 (elt tokens (+ i 1))
323 (elt tokens (+ i 2)))
324 (setq i (+ i 3))))))
325
326 (defun context-coloring-parse-array (input)
327 "Specialized JSON parser for a flat array of numbers."
328 (vconcat (mapcar 'string-to-number (split-string (substring input 1 -1) ","))))
329
330 (defun context-coloring-kill-scopifier ()
331 "Kills the currently-running scopifier process for this
332 buffer."
333 (when (not (null context-coloring-scopifier-process))
334 (delete-process context-coloring-scopifier-process)
335 (setq context-coloring-scopifier-process nil)))
336
337 (defun context-coloring-scopify-shell-command (command &optional callback)
338 "Invokes a scopifier with the current buffer's contents,
339 reading the scopifier's response asynchronously and applying a
340 parsed list of tokens to `context-coloring-apply-tokens'.
341
342 Invokes CALLBACK when complete."
343
344 ;; Prior running tokenization is implicitly obsolete if this function is
345 ;; called.
346 (context-coloring-kill-scopifier)
347
348 ;; Start the process.
349 (setq context-coloring-scopifier-process
350 (start-process-shell-command "scopifier" nil command))
351
352 (let ((output "")
353 (buffer context-coloring-buffer))
354
355 ;; The process may produce output in multiple chunks. This filter
356 ;; accumulates the chunks into a message.
357 (set-process-filter
358 context-coloring-scopifier-process
359 (lambda (_process chunk)
360 (setq output (concat output chunk))))
361
362 ;; When the process's message is complete, this sentinel parses it as JSON
363 ;; and applies the tokens to the buffer.
364 (set-process-sentinel
365 context-coloring-scopifier-process
366 (lambda (_process event)
367 (when (equal "finished\n" event)
368 (let ((tokens (context-coloring-parse-array output)))
369 (with-current-buffer buffer
370 (context-coloring-apply-tokens tokens))
371 (setq context-coloring-scopifier-process nil)
372 (if callback (funcall callback)))))))
373
374 ;; Give the process its input so it can begin.
375 (process-send-region context-coloring-scopifier-process (point-min) (point-max))
376 (process-send-eof context-coloring-scopifier-process))
377
378
379 ;;; Dispatch
380
381 (defvar context-coloring-javascript-scopifier
382 `(:type shell-command
383 :executable "node"
384 :command ,(expand-file-name
385 "./languages/javascript/bin/scopifier"
386 context-coloring-path)))
387
388 (defvar context-coloring-js2-colorizer
389 `(:type elisp
390 :colorizer context-coloring-js2-colorize))
391
392 (defcustom context-coloring-dispatch-plist
393 `(js-mode ,context-coloring-javascript-scopifier
394 js2-mode ,context-coloring-js2-colorizer
395 js3-mode ,context-coloring-javascript-scopifier)
396 "Property list mapping major modes to scopification and
397 colorization programs."
398 :group 'context-coloring)
399
400 (defun context-coloring-dispatch (&optional callback)
401 "Determines the optimal track for scopification / colorization
402 of the current buffer, then executes it.
403
404 Invokes CALLBACK when complete. It is invoked synchronously for
405 elisp tracks, and asynchronously for shell command tracks."
406 (let ((dispatch (plist-get context-coloring-dispatch-plist major-mode)))
407 (if (null dispatch)
408 (message "%s" "Context coloring is not available for this major mode"))
409 (let ((type (plist-get dispatch :type)))
410 (cond
411 ((eq type 'elisp)
412 (let ((colorizer (plist-get dispatch :colorizer))
413 (scopifier (plist-get dispatch :scopifier)))
414 (cond
415 (colorizer
416 (funcall colorizer)
417 (if callback (funcall callback)))
418 (scopifier
419 (context-coloring-apply-tokens (funcall scopifier))
420 (if callback (funcall callback)))
421 (t
422 (error "No `:colorizer' nor `:scopifier' specified for dispatch of `:type' elisp")))))
423 ((eq type 'shell-command)
424 (let ((executable (plist-get dispatch :executable))
425 (command (plist-get dispatch :command)))
426 (if (null command)
427 (error "No `:command' specified for dispatch of `:type' shell-command"))
428 (if (and (not (null executable))
429 (null (executable-find executable)))
430 (message "Executable \"%s\" not found" executable))
431 (context-coloring-scopify-shell-command command callback)))))))
432
433
434 ;;; Colorization
435
436 (defun context-coloring-colorize (&optional callback)
437 "Colors the current buffer by function context.
438
439 Invokes CALLBACK when complete; see `context-coloring-dispatch'."
440 (interactive)
441 (let ((start-time (float-time)))
442 (context-coloring-dispatch
443 (lambda ()
444 (when context-coloring-benchmark-colorization
445 (message "Colorization took %.3f seconds" (- (float-time) start-time)))
446 (if callback (funcall callback))))))
447
448 (defun context-coloring-change-function (_start _end _length)
449 "Registers a change so that a context-colored buffer can be
450 colorized soon."
451 ;; Tokenization is obsolete if there was a change.
452 (context-coloring-kill-scopifier)
453 (setq context-coloring-changed t))
454
455 (defun context-coloring-maybe-colorize ()
456 "Colorize unders certain conditions. This will run as an idle
457 timer, so firstly the buffer must not be some other
458 buffer. Additionally, the buffer must have changed, otherwise
459 colorizing would be redundant."
460 (when (and (eq context-coloring-buffer (window-buffer (selected-window)))
461 context-coloring-changed)
462 (setq context-coloring-changed nil)
463 (context-coloring-colorize)))
464
465
466 ;;; Minor mode
467
468 ;;;###autoload
469 (define-minor-mode context-coloring-mode
470 "Context-based code coloring, inspired by Douglas Crockford."
471 nil " Context" nil
472 (if (not context-coloring-mode)
473 (progn
474 (context-coloring-kill-scopifier)
475 (when context-coloring-colorize-idle-timer
476 (cancel-timer context-coloring-colorize-idle-timer))
477 (remove-hook 'js2-post-parse-callbacks 'context-coloring-colorize t)
478 (remove-hook 'after-change-functions 'context-coloring-change-function t)
479 (font-lock-mode)
480 (jit-lock-mode t))
481
482 ;; Remember this buffer. This value should not be dynamically-bound.
483 (setq context-coloring-buffer (current-buffer))
484
485 ;; Font lock is incompatible with this mode; the converse is also true.
486 (font-lock-mode 0)
487 (jit-lock-mode nil)
488
489 ;; Colorize once initially.
490 (context-coloring-colorize)
491
492 (cond
493 ((equal major-mode 'js2-mode)
494 ;; Only recolor on reparse.
495 (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t))
496 (t
497 ;; Only recolor on change.
498 (add-hook 'after-change-functions 'context-coloring-change-function nil t)))
499
500 (when (not (equal major-mode 'js2-mode))
501 ;; Only recolor idly.
502 (setq context-coloring-colorize-idle-timer
503 (run-with-idle-timer context-coloring-delay t 'context-coloring-maybe-colorize)))))
504
505 (provide 'context-coloring)
506
507 ;;; context-coloring.el ends here