]> code.delx.au - gnu-emacs-elpa/blob - packages/context-coloring/context-coloring.el
Merge commit '50127e91c39a8c72eb2ea1ffadf708f31699cf84'
[gnu-emacs-elpa] / packages / context-coloring / context-coloring.el
1 ;;; context-coloring.el --- Syntax highlighting, except not for syntax. -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2014-2015 Free Software Foundation, Inc.
4
5 ;; Author: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
6 ;; URL: https://github.com/jacksonrayhamilton/context-coloring
7 ;; Keywords: context coloring syntax highlighting
8 ;; Version: 4.0.0
9 ;; Package-Requires: ((emacs "24") (js2-mode "20150126"))
10
11 ;; This file is part of GNU Emacs.
12
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.
17
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.
22
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/>.
25
26 ;;; Commentary:
27
28 ;; Highlights code according to function context.
29
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
32 ;; color, and so on.
33 ;; - Identifiers retain the color of the scope in which they are declared.
34
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.
39
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.
43
44 ;; To use, add the following to your ~/.emacs:
45
46 ;; (require 'context-coloring)
47 ;; (add-hook 'js2-mode-hook 'context-coloring-mode)
48
49 ;; js-mode or js3-mode support requires Node.js 0.10+ and the scopifier
50 ;; executable.
51
52 ;; $ npm install -g scopifier
53
54 ;;; Code:
55
56 (require 'js2-mode)
57
58
59 ;;; Constants
60
61 (defconst context-coloring-path
62 (file-name-directory (or load-file-name buffer-file-name))
63 "This file's directory.")
64
65
66 ;;; Customizable options
67
68 (defcustom context-coloring-delay 0.25
69 "Delay between a buffer update and colorization.
70
71 Increase this if your machine is high-performing. Decrease it if
72 it ain't.
73
74 Supported modes: `js-mode', `js3-mode'"
75 :group 'context-coloring)
76
77 (defcustom context-coloring-comments-and-strings t
78 "If non-nil, also color comments and strings using `font-lock'."
79 :group 'context-coloring)
80
81 (defcustom context-coloring-js-block-scopes nil
82 "If non-nil, also color block scopes in the scope hierarchy in JavaScript.
83
84 The block-scope-inducing `let' and `const' are introduced in ES6.
85 If you are writing ES6 code, enable this; otherwise, don't.
86
87 Supported modes: `js2-mode'"
88 :group 'context-coloring)
89
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)
94
95
96 ;;; Local variables
97
98 (defvar-local context-coloring-buffer nil
99 "Reference to this buffer (for timers).")
100
101 (defvar-local context-coloring-scopifier-process nil
102 "Reference to the single scopifier process that can be
103 running.")
104
105 (defvar-local context-coloring-colorize-idle-timer nil
106 "Reference to the currently-running idle timer.")
107
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
112 used.")
113
114
115 ;;; Faces
116
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)))
124 ,doc
125 :group 'context-coloring)))))
126
127 (defvar context-coloring-face-count nil
128 "Number of faces available for context coloring.")
129
130 (defun context-coloring-defface-default (level)
131 (context-coloring-defface level "white" "#3f3f3f" "#cdcdcd"))
132
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))
143
144 (context-coloring-set-colors-default)
145
146
147 ;;; Face functions
148
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)
154 "-face")))
155
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))
160 (let ((level 0))
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)))))
167
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)))
171
172
173 ;;; Colorization utilities
174
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."
178 (add-text-properties
179 start
180 end
181 `(face ,(context-coloring-level-face level))))
182
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
187 (save-excursion
188 (font-lock-fontify-syntactically-region (point-min) (point-max)))))
189
190
191 ;;; js2-mode colorization
192
193 (defvar-local context-coloring-js2-scope-level-hash-table nil
194 "Associates `js2-scope' structures and with their scope
195 levels.")
196
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))
200 (t
201 (let ((level 0)
202 (current-scope scope)
203 enclosing-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)))))
216
217 (defsubst context-coloring-js2-local-name-node-p (node)
218 "Determines if NODE is a js2-name-node representing a local
219 variable."
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))))))))
228
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
233 start
234 (+ start (js2-node-len node)) ; End
235 level)))
236
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
243 (js2-visit-ast
244 js2-mode-ast
245 (lambda (node end-p)
246 (when (null end-p)
247 (cond
248 ((js2-scope-p node)
249 (context-coloring-js2-colorize-node
250 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
255 enclosing-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
260 ;; don't do it.
261 (when (not (eq defining-scope enclosing-scope))
262 (context-coloring-js2-colorize-node
263 node
264 (context-coloring-js2-scope-level defining-scope))))))
265 ;; The `t' indicates to search children.
266 t)))
267 (context-coloring-maybe-colorize-comments-and-strings)))
268
269
270 ;;; Shell command scopification / colorization
271
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
276 element."
277 (with-silent-modifications
278 (let ((i 0)
279 (len (length tokens)))
280 (while (< i len)
281 (context-coloring-colorize-region
282 (elt tokens i)
283 (elt tokens (+ i 1))
284 (elt tokens (+ i 2)))
285 (setq i (+ i 3))))
286 (context-coloring-maybe-colorize-comments-and-strings)))
287
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) ","))))
291
292 (defun context-coloring-kill-scopifier ()
293 "Kills the currently-running scopifier process for this
294 buffer."
295 (when (not (null context-coloring-scopifier-process))
296 (delete-process context-coloring-scopifier-process)
297 (setq context-coloring-scopifier-process nil)))
298
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'.
303
304 Invokes CALLBACK when complete."
305
306 ;; Prior running tokenization is implicitly obsolete if this function is
307 ;; called.
308 (context-coloring-kill-scopifier)
309
310 ;; Start the process.
311 (setq context-coloring-scopifier-process
312 (start-process-shell-command "scopifier" nil command))
313
314 (let ((output "")
315 (buffer context-coloring-buffer))
316
317 ;; The process may produce output in multiple chunks. This filter
318 ;; accumulates the chunks into a message.
319 (set-process-filter
320 context-coloring-scopifier-process
321 (lambda (_process chunk)
322 (setq output (concat output chunk))))
323
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)))))))
335
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))
339
340
341 ;;; Dispatch
342
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.")
346
347 (defvar context-coloring-mode-hash-table (make-hash-table :test 'eq)
348 "Mapping of major mode names to dispatch property lists.")
349
350 (defun context-coloring-select-dispatch (mode dispatch)
351 "Use DISPATCH for MODE."
352 (puthash
353 mode
354 (gethash
355 dispatch
356 context-coloring-dispatch-hash-table)
357 context-coloring-mode-hash-table))
358
359 (defun context-coloring-define-dispatch (symbol &rest properties)
360 "Define a new dispatch named SYMBOL with PROPERTIES.
361
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
368 color the buffer.
369
370 PROPERTIES must include `:modes' and one of `:colorizer',
371 `:scopifier' or `:command'.
372
373 `:modes' - List of major modes this dispatch is valid for.
374
375 `:colorizer' - Symbol referring to a function that parses and
376 colors the buffer.
377
378 `:scopifier' - Symbol referring to a function that parses the
379 buffer a returns a flat vector of start, end and level data.
380
381 `:executable' - Optional name of an executable required by
382 `:command'.
383
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)))
391 (when (null modes)
392 (error "No mode defined for dispatch"))
393 (when (not (or colorizer
394 scopifier
395 command))
396 (error "No colorizer, scopifier or command defined for dispatch"))
397 (puthash symbol properties context-coloring-dispatch-hash-table)
398 (dolist (mode modes)
399 (when (null (gethash mode context-coloring-mode-hash-table))
400 (puthash mode properties context-coloring-mode-hash-table)))))
401
402 (context-coloring-define-dispatch
403 'javascript-node
404 :modes '(js-mode js3-mode)
405 :executable "scopifier"
406 :command "scopifier")
407
408 (context-coloring-define-dispatch
409 'javascript-js2
410 :modes '(js2-mode)
411 :colorizer 'context-coloring-js2-colorize)
412
413 (defun context-coloring-dispatch (&optional callback)
414 "Determines the optimal track for scopification / colorization
415 of the current buffer, then executes it.
416
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)))
420 (if (null dispatch)
421 (message "%s" "Context coloring is not available for this major mode"))
422 (let (colorizer
423 scopifier
424 command
425 executable)
426 (cond
427 ((setq colorizer (plist-get dispatch :colorizer))
428 (funcall 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))
435 (if (and executable
436 (null (executable-find executable)))
437 (message "Executable \"%s\" not found" executable)
438 (context-coloring-scopify-shell-command command callback)))))))
439
440
441 ;;; Colorization
442
443 (defun context-coloring-colorize (&optional callback)
444 "Colors the current buffer by function context.
445
446 Invokes CALLBACK when complete; see `context-coloring-dispatch'."
447 (interactive)
448 (let ((start-time (float-time)))
449 (context-coloring-dispatch
450 (lambda ()
451 (when context-coloring-benchmark-colorization
452 (message "Colorization took %.3f seconds" (- (float-time) start-time)))
453 (if callback (funcall callback))))))
454
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))
460
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
465 would be redundant."
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)))
470
471
472 ;;; Themes
473
474 (defvar context-coloring-theme-hash-table (make-hash-table :test 'eq)
475 "Mapping of theme names to theme properties.")
476
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:
480
481 `:colors': List of colors that this theme uses."
482 (puthash
483 theme
484 (lambda ()
485 (apply 'context-coloring-set-colors (plist-get properties :colors)))
486 context-coloring-theme-hash-table))
487
488 (defun context-coloring-load-theme (theme)
489 "Apply THEME's colors and other properties for context
490 coloring."
491 (let ((function (gethash theme context-coloring-theme-hash-table)))
492 (when (null function)
493 (error (format "No such theme `%s'" theme)))
494 (funcall function)))
495
496 (context-coloring-define-theme
497 'monokai
498 :colors '("#F8F8F2"
499 "#66D9EF"
500 "#A1EFE4"
501 "#A6E22E"
502 "#E6DB74"
503 "#FD971F"
504 "#F92672"
505 "#FD5FF0"
506 "#AE81FF"))
507
508 (context-coloring-define-theme
509 'solarized
510 :colors '("#839496"
511 "#268bd2"
512 "#2aa198"
513 "#859900"
514 "#b58900"
515 "#cb4b16"
516 "#dc322f"
517 "#d33682"
518 "#6c71c4"
519 "#69B7F0"
520 "#69CABF"
521 "#B4C342"
522 "#DEB542"
523 "#F2804F"
524 "#FF6E64"
525 "#F771AC"
526 "#9EA0E5"))
527
528 (context-coloring-define-theme
529 'tango
530 :colors '("#2e3436"
531 "#346604"
532 "#204a87"
533 "#5c3566"
534 "#a40000"
535 "#b35000"
536 "#c4a000"
537 "#8ae234"
538 "#8cc4ff"
539 "#ad7fa8"
540 "#ef2929"
541 "#fcaf3e"
542 "#fce94f"))
543
544 (context-coloring-define-theme
545 'zenburn
546 :colors '("#DCDCCC"
547 "#93E0E3"
548 "#BFEBBF"
549 "#F0DFAF"
550 "#DFAF8F"
551 "#CC9393"
552 "#DC8CC3"
553 "#94BFF3"
554 "#9FC59F"
555 "#D0BF8F"
556 "#DCA3A3"))
557
558
559 ;;; Minor mode
560
561 ;;;###autoload
562 (define-minor-mode context-coloring-mode
563 "Context-based code coloring, inspired by Douglas Crockford."
564 nil " Context" nil
565 (if (not context-coloring-mode)
566 (progn
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)
572 (font-lock-mode)
573 (jit-lock-mode t))
574
575 ;; Remember this buffer. This value should not be dynamically-bound.
576 (setq context-coloring-buffer (current-buffer))
577
578 ;; Font lock is incompatible with this mode; the converse is also true.
579 (font-lock-mode 0)
580 (jit-lock-mode nil)
581
582 ;; Colorize once initially.
583 (context-coloring-colorize)
584
585 (cond
586 ((equal major-mode 'js2-mode)
587 ;; Only recolor on reparse.
588 (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t))
589 (t
590 ;; Only recolor on change.
591 (add-hook 'after-change-functions 'context-coloring-change-function nil t)))
592
593 (when (not (equal major-mode 'js2-mode))
594 ;; Only recolor idly.
595 (setq context-coloring-colorize-idle-timer
596 (run-with-idle-timer
597 context-coloring-delay
598 t
599 'context-coloring-maybe-colorize)))))
600
601 (provide 'context-coloring)
602
603 ;; Local Variables:
604 ;; eval: (when (fboundp 'rainbow-mode) (rainbow-mode 1))
605 ;; End:
606
607 ;;; context-coloring.el ends here