]> code.delx.au - gnu-emacs-elpa/blob - packages/context-coloring/context-coloring.el
Merge commit '68bcaa8d8df5518217a3833fd1bb400c8225fe02' from hydra
[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.1.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 ;; 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.
148 (dotimes (number 18)
149 (context-coloring-defface-default (+ number context-coloring-face-count)))
150
151
152 ;;; Face functions
153
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)
159 "-face")))
160
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))
165 (let ((level 0))
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)))))
172
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)))
176
177
178 ;;; Colorization utilities
179
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."
183 (add-text-properties
184 start
185 end
186 `(face ,(context-coloring-level-face level))))
187
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
192 (save-excursion
193 (font-lock-fontify-syntactically-region (point-min) (point-max)))))
194
195
196 ;;; js2-mode colorization
197
198 (defvar-local context-coloring-js2-scope-level-hash-table nil
199 "Associates `js2-scope' structures and with their scope
200 levels.")
201
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))
205 (t
206 (let ((level 0)
207 (current-scope scope)
208 enclosing-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)))))
221
222 (defsubst context-coloring-js2-local-name-node-p (node)
223 "Determines if NODE is a js2-name-node representing a local
224 variable."
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))))))))
233
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
238 start
239 (+ start (js2-node-len node)) ; End
240 level)))
241
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
248 (js2-visit-ast
249 js2-mode-ast
250 (lambda (node end-p)
251 (when (null end-p)
252 (cond
253 ((js2-scope-p node)
254 (context-coloring-js2-colorize-node
255 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
260 enclosing-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
265 ;; don't do it.
266 (when (not (eq defining-scope enclosing-scope))
267 (context-coloring-js2-colorize-node
268 node
269 (context-coloring-js2-scope-level defining-scope))))))
270 ;; The `t' indicates to search children.
271 t)))
272 (context-coloring-maybe-colorize-comments-and-strings)))
273
274
275 ;;; Shell command scopification / colorization
276
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
281 element."
282 (with-silent-modifications
283 (let ((i 0)
284 (len (length tokens)))
285 (while (< i len)
286 (context-coloring-colorize-region
287 (elt tokens i)
288 (elt tokens (+ i 1))
289 (elt tokens (+ i 2)))
290 (setq i (+ i 3))))
291 (context-coloring-maybe-colorize-comments-and-strings)))
292
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) ","))))
296
297 (defun context-coloring-kill-scopifier ()
298 "Kills the currently-running scopifier process for this
299 buffer."
300 (when (not (null context-coloring-scopifier-process))
301 (delete-process context-coloring-scopifier-process)
302 (setq context-coloring-scopifier-process nil)))
303
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'.
308
309 Invokes CALLBACK when complete."
310
311 ;; Prior running tokenization is implicitly obsolete if this function is
312 ;; called.
313 (context-coloring-kill-scopifier)
314
315 ;; Start the process.
316 (setq context-coloring-scopifier-process
317 (start-process-shell-command "scopifier" nil command))
318
319 (let ((output "")
320 (buffer context-coloring-buffer))
321
322 ;; The process may produce output in multiple chunks. This filter
323 ;; accumulates the chunks into a message.
324 (set-process-filter
325 context-coloring-scopifier-process
326 (lambda (_process chunk)
327 (setq output (concat output chunk))))
328
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)))))))
340
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))
344
345
346 ;;; Dispatch
347
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.")
351
352 (defvar context-coloring-mode-hash-table (make-hash-table :test 'eq)
353 "Mapping of major mode names to dispatch property lists.")
354
355 (defun context-coloring-select-dispatch (mode dispatch)
356 "Use DISPATCH for MODE."
357 (puthash
358 mode
359 (gethash
360 dispatch
361 context-coloring-dispatch-hash-table)
362 context-coloring-mode-hash-table))
363
364 (defun context-coloring-define-dispatch (symbol &rest properties)
365 "Define a new dispatch named SYMBOL with PROPERTIES.
366
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
373 color the buffer.
374
375 PROPERTIES must include `:modes' and one of `:colorizer',
376 `:scopifier' or `:command'.
377
378 `:modes' - List of major modes this dispatch is valid for.
379
380 `:colorizer' - Symbol referring to a function that parses and
381 colors the buffer.
382
383 `:scopifier' - Symbol referring to a function that parses the
384 buffer a returns a flat vector of start, end and level data.
385
386 `:executable' - Optional name of an executable required by
387 `:command'.
388
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)))
396 (when (null modes)
397 (error "No mode defined for dispatch"))
398 (when (not (or colorizer
399 scopifier
400 command))
401 (error "No colorizer, scopifier or command defined for dispatch"))
402 (puthash symbol properties context-coloring-dispatch-hash-table)
403 (dolist (mode modes)
404 (when (null (gethash mode context-coloring-mode-hash-table))
405 (puthash mode properties context-coloring-mode-hash-table)))))
406
407 (context-coloring-define-dispatch
408 'javascript-node
409 :modes '(js-mode js3-mode)
410 :executable "scopifier"
411 :command "scopifier")
412
413 (context-coloring-define-dispatch
414 'javascript-js2
415 :modes '(js2-mode)
416 :colorizer 'context-coloring-js2-colorize)
417
418 (defun context-coloring-dispatch (&optional callback)
419 "Determines the optimal track for scopification / colorization
420 of the current buffer, then executes it.
421
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)))
425 (if (null dispatch)
426 (message "%s" "Context coloring is not available for this major mode"))
427 (let (colorizer
428 scopifier
429 command
430 executable)
431 (cond
432 ((setq colorizer (plist-get dispatch :colorizer))
433 (funcall 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))
440 (if (and executable
441 (null (executable-find executable)))
442 (message "Executable \"%s\" not found" executable)
443 (context-coloring-scopify-shell-command command callback)))))))
444
445
446 ;;; Colorization
447
448 (defun context-coloring-colorize (&optional callback)
449 "Colors the current buffer by function context.
450
451 Invokes CALLBACK when complete; see `context-coloring-dispatch'."
452 (interactive)
453 (let ((start-time (float-time)))
454 (context-coloring-dispatch
455 (lambda ()
456 (when context-coloring-benchmark-colorization
457 (message "Colorization took %.3f seconds" (- (float-time) start-time)))
458 (if callback (funcall callback))))))
459
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))
465
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
470 would be redundant."
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)))
475
476
477 ;;; Themes
478
479 (defvar context-coloring-theme-hash-table (make-hash-table :test 'eq)
480 "Mapping of theme names to theme properties.")
481
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?
490 (let ((level -1))
491 ;; AFAIK, no way to know if a theme already has a face set, so just
492 ;; override blindly for now.
493 (apply
494 'custom-theme-set-faces
495 theme
496 (mapcar
497 (lambda (color)
498 (setq level (+ level 1))
499 `(,(context-coloring-face-symbol level) ((t (:foreground ,color)))))
500 colors))))))
501
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:
505
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)))))
513
514 (defun context-coloring-load-theme (&optional rest)
515 (declare (obsolete
516 "themes are now loaded alongside custom themes automatically."
517 "4.1.0")))
518
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)))
524
525 (context-coloring-define-theme
526 'leuven
527 :colors '("#333333"
528 "#0000FF"
529 "#6434A3"
530 "#BA36A5"
531 "#D0372D"
532 "#036A07"
533 "#006699"
534 "#006FE0"
535 "#808080"))
536
537 (context-coloring-define-theme
538 'monokai
539 :colors '("#F8F8F2"
540 "#66D9EF"
541 "#A1EFE4"
542 "#A6E22E"
543 "#E6DB74"
544 "#FD971F"
545 "#F92672"
546 "#FD5FF0"
547 "#AE81FF"))
548
549 (context-coloring-define-theme
550 'solarized
551 :aliases '(solarized-light
552 solarized-dark
553 sanityinc-solarized-light
554 sanityinc-solarized-dark)
555 :colors '("#839496"
556 "#268bd2"
557 "#2aa198"
558 "#859900"
559 "#b58900"
560 "#cb4b16"
561 "#dc322f"
562 "#d33682"
563 "#6c71c4"
564 "#69B7F0"
565 "#69CABF"
566 "#B4C342"
567 "#DEB542"
568 "#F2804F"
569 "#FF6E64"
570 "#F771AC"
571 "#9EA0E5"))
572
573 (context-coloring-define-theme
574 'tango
575 :colors '("#2e3436"
576 "#346604"
577 "#204a87"
578 "#5c3566"
579 "#a40000"
580 "#b35000"
581 "#c4a000"
582 "#8ae234"
583 "#8cc4ff"
584 "#ad7fa8"
585 "#ef2929"
586 "#fcaf3e"
587 "#fce94f"))
588
589 (context-coloring-define-theme
590 'zenburn
591 :colors '("#DCDCCC"
592 "#93E0E3"
593 "#BFEBBF"
594 "#F0DFAF"
595 "#DFAF8F"
596 "#CC9393"
597 "#DC8CC3"
598 "#94BFF3"
599 "#9FC59F"
600 "#D0BF8F"
601 "#DCA3A3"))
602
603
604 ;;; Minor mode
605
606 ;;;###autoload
607 (define-minor-mode context-coloring-mode
608 "Context-based code coloring, inspired by Douglas Crockford."
609 nil " Context" nil
610 (if (not context-coloring-mode)
611 (progn
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)
617 (font-lock-mode)
618 (jit-lock-mode t))
619
620 ;; Remember this buffer. This value should not be dynamically-bound.
621 (setq context-coloring-buffer (current-buffer))
622
623 ;; Font lock is incompatible with this mode; the converse is also true.
624 (font-lock-mode 0)
625 (jit-lock-mode nil)
626
627 ;; Colorize once initially.
628 (context-coloring-colorize)
629
630 (cond
631 ((equal major-mode 'js2-mode)
632 ;; Only recolor on reparse.
633 (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t))
634 (t
635 ;; Only recolor on change.
636 (add-hook 'after-change-functions 'context-coloring-change-function nil t)))
637
638 (when (not (equal major-mode 'js2-mode))
639 ;; Only recolor idly.
640 (setq context-coloring-colorize-idle-timer
641 (run-with-idle-timer
642 context-coloring-delay
643 t
644 'context-coloring-maybe-colorize)))))
645
646 (provide 'context-coloring)
647
648 ;; Local Variables:
649 ;; eval: (when (fboundp 'rainbow-mode) (rainbow-mode 1))
650 ;; End:
651
652 ;;; context-coloring.el ends here