]> code.delx.au - gnu-emacs-elpa/blob - context-coloring.el
Pass quote test with recursive colorizer.
[gnu-emacs-elpa] / context-coloring.el
1 ;;; context-coloring.el --- Highlight by scope -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2014-2015 Free Software Foundation, Inc.
4
5 ;; Author: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
6 ;; Version: 6.3.0
7 ;; Keywords: convenience faces tools
8 ;; Package-Requires: ((emacs "24") (js2-mode "20150126"))
9 ;; URL: https://github.com/jacksonrayhamilton/context-coloring
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 by scope. Top-level scopes are one color, second-level
29 ;; scopes are another color, and so on. Variables retain the color of the scope
30 ;; in which they are defined. A variable defined in an outer scope referenced
31 ;; by an inner scope is colored the same as the outer scope.
32
33 ;; By default, comments and strings are still highlighted syntactically.
34
35 ;;; Code:
36
37 (require 'js2-mode)
38
39
40 ;;; Utilities
41
42 (defun context-coloring-join (strings delimiter)
43 "Join a list of STRINGS with the string DELIMITER."
44 (mapconcat 'identity strings delimiter))
45
46 (defsubst context-coloring-trim-right (string)
47 "Remove leading whitespace from STRING."
48 (if (string-match "[ \t\n\r]+\\'" string)
49 (replace-match "" t t string)
50 string))
51
52 (defsubst context-coloring-trim-left (string)
53 "Remove trailing whitespace from STRING."
54 (if (string-match "\\`[ \t\n\r]+" string)
55 (replace-match "" t t string)
56 string))
57
58 (defsubst context-coloring-trim (string)
59 "Remove leading and trailing whitespace from STRING."
60 (context-coloring-trim-left (context-coloring-trim-right string)))
61
62
63 ;;; Faces
64
65 (defun context-coloring-defface (level tty light dark)
66 "Define a face for LEVEL with colors for TTY, LIGHT and DARK
67 backgrounds."
68 (let ((face (intern (format "context-coloring-level-%s-face" level)))
69 (doc (format "Context coloring face, level %s." level)))
70 (custom-declare-face
71 face
72 `((((type tty)) (:foreground ,tty))
73 (((background light)) (:foreground ,light))
74 (((background dark)) (:foreground ,dark)))
75 doc
76 :group 'context-coloring)))
77
78 (defun context-coloring-defface-neutral (level)
79 "Define a face for LEVEL with the default neutral colors."
80 (context-coloring-defface level nil "#3f3f3f" "#cdcdcd"))
81
82 (context-coloring-defface 0 nil "#000000" "#ffffff")
83 (context-coloring-defface 1 "yellow" "#008b8b" "#00ffff")
84 (context-coloring-defface 2 "green" "#0000ff" "#87cefa")
85 (context-coloring-defface 3 "cyan" "#483d8b" "#b0c4de")
86 (context-coloring-defface 4 "blue" "#a020f0" "#eedd82")
87 (context-coloring-defface 5 "magenta" "#a0522d" "#98fb98")
88 (context-coloring-defface 6 "red" "#228b22" "#7fffd4")
89 (context-coloring-defface-neutral 7)
90
91 (defvar context-coloring-maximum-face nil
92 "Index of the highest face available for coloring.")
93
94 (defvar context-coloring-original-maximum-face nil
95 "Fallback value for `context-coloring-maximum-face' when all
96 themes have been disabled.")
97
98 (setq context-coloring-maximum-face 7)
99
100 (setq context-coloring-original-maximum-face
101 context-coloring-maximum-face)
102
103 ;; Theme authors can have up to 26 levels: 1 (0th) for globals, 24 (1st-24th)
104 ;; for nested levels, and 1 (25th) for infinity.
105 (dotimes (number 18)
106 (context-coloring-defface-neutral (+ number context-coloring-maximum-face 1)))
107
108
109 ;;; Face functions
110
111 (defsubst context-coloring-level-face (level)
112 "Return the symbol for a face with LEVEL."
113 ;; `concat' is faster than `format' here.
114 (intern-soft
115 (concat "context-coloring-level-" (number-to-string level) "-face")))
116
117 (defsubst context-coloring-bounded-level-face (level)
118 "Return the symbol for a face with LEVEL, bounded by
119 `context-coloring-maximum-face'."
120 (context-coloring-level-face (min level context-coloring-maximum-face)))
121
122
123 ;;; Colorization utilities
124
125 (defsubst context-coloring-colorize-region (start end level)
126 "Color characters from the 1-indexed START point (inclusive) to
127 the END point (exclusive) with the face corresponding to LEVEL."
128 (add-text-properties
129 start
130 end
131 `(face ,(context-coloring-bounded-level-face level))))
132
133 (make-obsolete-variable
134 'context-coloring-comments-and-strings
135 "use `context-coloring-syntactic-comments' and
136 `context-coloring-syntactic-strings' instead."
137 "6.1.0")
138
139 (defcustom context-coloring-syntactic-comments t
140 "If non-nil, also color comments using `font-lock'."
141 :group 'context-coloring)
142
143 (defcustom context-coloring-syntactic-strings t
144 "If non-nil, also color strings using `font-lock'."
145 :group 'context-coloring)
146
147 (defun context-coloring-font-lock-syntactic-comment-function (state)
148 "Tell `font-lock' to color a comment but not a string."
149 (if (nth 3 state) nil font-lock-comment-face))
150
151 (defun context-coloring-font-lock-syntactic-string-function (state)
152 "Tell `font-lock' to color a string but not a comment."
153 (if (nth 3 state) font-lock-string-face nil))
154
155 (defsubst context-coloring-maybe-colorize-comments-and-strings (&optional min max)
156 "Color the current buffer's comments or strings if
157 `context-coloring-syntactic-comments' or
158 `context-coloring-syntactic-strings' are non-nil."
159 (when (or context-coloring-syntactic-comments
160 context-coloring-syntactic-strings)
161 (let ((min (or min (point-min)))
162 (max (or max (point-max)))
163 (font-lock-syntactic-face-function
164 (cond
165 ((and context-coloring-syntactic-comments
166 (not context-coloring-syntactic-strings))
167 'context-coloring-font-lock-syntactic-comment-function)
168 ((and context-coloring-syntactic-strings
169 (not context-coloring-syntactic-comments))
170 'context-coloring-font-lock-syntactic-string-function)
171 (t
172 font-lock-syntactic-face-function))))
173 (save-excursion
174 (font-lock-fontify-syntactically-region min max)
175 ;; TODO: Make configurable at the dispatch level.
176 (when (eq major-mode 'emacs-lisp-mode)
177 (font-lock-fontify-keywords-region min max))))))
178
179
180 ;;; js2-mode colorization
181
182 (defvar-local context-coloring-js2-scope-level-hash-table nil
183 "Associate `js2-scope' structures and with their scope
184 levels.")
185
186 (defcustom context-coloring-js-block-scopes nil
187 "If non-nil, also color block scopes in the scope hierarchy in JavaScript.
188
189 The block-scoped `let' and `const' are introduced in ES6. Enable
190 this for ES6 code; disable it elsewhere.
191
192 Supported modes: `js2-mode'"
193 :group 'context-coloring)
194
195 (defsubst context-coloring-js2-scope-level (scope)
196 "Return the level of SCOPE."
197 (cond ((gethash scope context-coloring-js2-scope-level-hash-table))
198 (t
199 (let ((level 0)
200 (current-scope scope)
201 enclosing-scope)
202 (while (and current-scope
203 (js2-node-parent current-scope)
204 (setq enclosing-scope
205 (js2-node-get-enclosing-scope current-scope)))
206 (when (or context-coloring-js-block-scopes
207 (let ((type (js2-scope-type current-scope)))
208 (or (= type js2-SCRIPT)
209 (= type js2-FUNCTION)
210 (= type js2-CATCH))))
211 (setq level (+ level 1)))
212 (setq current-scope enclosing-scope))
213 (puthash scope level context-coloring-js2-scope-level-hash-table)))))
214
215 (defsubst context-coloring-js2-local-name-node-p (node)
216 "Determine if NODE is a `js2-name-node' representing a local
217 variable."
218 (and (js2-name-node-p node)
219 (let ((parent (js2-node-parent node)))
220 (not (or (and (js2-object-prop-node-p parent)
221 (eq node (js2-object-prop-node-left parent)))
222 (and (js2-prop-get-node-p parent)
223 ;; For nested property lookup, the node on the left is a
224 ;; `js2-prop-get-node', so this always works.
225 (eq node (js2-prop-get-node-right parent))))))))
226
227 (defvar-local context-coloring-point-max nil
228 "Cached value of `point-max'.")
229
230 (defsubst context-coloring-js2-colorize-node (node level)
231 "Color NODE with the color for LEVEL."
232 (let ((start (js2-node-abs-pos node)))
233 (context-coloring-colorize-region
234 start
235 (min
236 ;; End
237 (+ start (js2-node-len node))
238 ;; Somes nodes (like the ast when there is an unterminated multiline
239 ;; comment) will stretch to the value of `point-max'.
240 context-coloring-point-max)
241 level)))
242
243 (defun context-coloring-js2-colorize ()
244 "Color the current buffer using the abstract syntax tree
245 generated by `js2-mode'."
246 ;; Reset the hash table; the old one could be obsolete.
247 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test 'eq))
248 (setq context-coloring-point-max (point-max))
249 (with-silent-modifications
250 (js2-visit-ast
251 js2-mode-ast
252 (lambda (node end-p)
253 (when (null end-p)
254 (cond
255 ((js2-scope-p node)
256 (context-coloring-js2-colorize-node
257 node
258 (context-coloring-js2-scope-level node)))
259 ((context-coloring-js2-local-name-node-p node)
260 (let* ((enclosing-scope (js2-node-get-enclosing-scope node))
261 (defining-scope (js2-get-defining-scope
262 enclosing-scope
263 (js2-name-node-name node))))
264 ;; The tree seems to be walked lexically, so an entire scope will
265 ;; be colored, including its name nodes, before they are reached.
266 ;; Coloring the nodes defined in that scope would be redundant, so
267 ;; don't do it.
268 (when (not (eq defining-scope enclosing-scope))
269 (context-coloring-js2-colorize-node
270 node
271 (context-coloring-js2-scope-level defining-scope))))))
272 ;; The `t' indicates to search children.
273 t)))
274 (context-coloring-maybe-colorize-comments-and-strings)))
275
276
277 ;;; Emacs Lisp colorization
278
279 (defsubst context-coloring-make-scope (depth level)
280 (list
281 :depth depth
282 :level level
283 :variables (make-hash-table)))
284
285 (defsubst context-coloring-scope-get-level (scope)
286 (plist-get scope :level))
287
288 (defsubst context-coloring-scope-add-variable (scope variable)
289 (puthash variable t (plist-get scope :variables)))
290
291 (defsubst context-coloring-scope-get-variable (scope variable)
292 (gethash variable (plist-get scope :variables)))
293
294 (defsubst context-coloring-get-variable-level (scope-stack variable)
295 (let* (scope
296 level)
297 (while (and scope-stack (not level))
298 (setq scope (car scope-stack))
299 (cond
300 ((context-coloring-scope-get-variable scope variable)
301 (setq level (context-coloring-scope-get-level scope)))
302 (t
303 (setq scope-stack (cdr scope-stack)))))
304 ;; Assume a global variable.
305 (or level 0)))
306
307 (defsubst context-coloring-make-backtick (end enabled)
308 (list
309 :end end
310 :enabled enabled))
311
312 (defsubst context-coloring-backtick-get-end (backtick)
313 (plist-get backtick :end))
314
315 (defsubst context-coloring-backtick-get-enabled (backtick)
316 (plist-get backtick :enabled))
317
318 (defsubst context-coloring-backtick-enabled-p (backtick-stack)
319 (context-coloring-backtick-get-enabled (car backtick-stack)))
320
321 (defsubst context-coloring-make-let-varlist (depth type)
322 (list
323 :depth depth
324 :type type
325 :vars '()))
326
327 (defsubst context-coloring-let-varlist-get-type (let-varlist)
328 (plist-get let-varlist :type))
329
330 (defsubst context-coloring-let-varlist-add-var (let-varlist var)
331 (plist-put let-varlist :vars (cons var (plist-get let-varlist :vars))))
332
333 (defsubst context-coloring-let-varlist-pop-vars (let-varlist)
334 (let ((type (context-coloring-let-varlist-get-type let-varlist))
335 (vars (plist-get let-varlist :vars)))
336 (cond
337 ;; `let' binds all at once at the end.
338 ((eq type 'let)
339 (prog1
340 vars
341 (plist-put let-varlist :vars '())))
342 ;; `let*' binds incrementally.
343 ((eq type 'let*)
344 (prog1
345 (list (car vars))
346 (plist-put let-varlist :vars (cdr vars)))))))
347
348 (defsubst context-coloring-forward-sws ()
349 "Move forward through whitespace and comments."
350 (while (forward-comment 1)))
351
352 (defsubst context-coloring-forward-sexp-position ()
353 "Like vanilla `forward-sexp', but just return the position."
354 (scan-sexps (point) 1))
355
356 (defsubst context-coloring-emacs-lisp-identifier-syntax-p (syntax-code)
357 (or (= 2 syntax-code)
358 (= 3 syntax-code)))
359
360 (defsubst context-coloring-open-parenthesis-p (syntax-code)
361 (= 4 syntax-code))
362
363 (defsubst context-coloring-close-parenthesis-p (syntax-code)
364 (= 5 syntax-code))
365
366 (defsubst context-coloring-expression-prefix-p (syntax-code)
367 (= 6 syntax-code))
368
369 (defsubst context-coloring-at-open-parenthesis-p ()
370 (= 4 (logand #xFFFF (car (syntax-after (point))))))
371
372 (defsubst context-coloring-ppss-depth (ppss)
373 ;; Same as (nth 0 ppss).
374 (car ppss))
375
376 (defsubst context-coloring-at-stack-depth-p (stack depth)
377 (= (plist-get (car stack) :depth) depth))
378
379 (defsubst context-coloring-exact-regexp (word)
380 "Create a regexp that matches exactly WORD."
381 (concat "\\`" (regexp-quote word) "\\'"))
382
383 (defsubst context-coloring-exact-or-regexp (words)
384 "Create a regexp that matches any exact word in WORDS."
385 (context-coloring-join
386 (mapcar 'context-coloring-exact-regexp words) "\\|"))
387
388 (defconst context-coloring-emacs-lisp-defun-regexp
389 (context-coloring-exact-or-regexp
390 '("defun" "defun*" "defsubst" "defmacro"
391 "cl-defun" "cl-defsubst" "cl-defmacro")))
392
393 (defconst context-coloring-emacs-lisp-lambda-regexp
394 (context-coloring-exact-regexp "lambda"))
395
396 (defconst context-coloring-emacs-lisp-let-regexp
397 (context-coloring-exact-regexp "let"))
398
399 (defconst context-coloring-emacs-lisp-let*-regexp
400 (context-coloring-exact-regexp "let*"))
401
402 (defconst context-coloring-emacs-lisp-arglist-arg-regexp
403 "\\`[^&:]")
404
405 (defconst context-coloring-ignored-word-regexp
406 (concat "\\`[&:-+]?[0-9]\\|" (context-coloring-exact-or-regexp
407 '("t" "nil" "." "?"))))
408
409 (defconst context-coloring-WORD-CODE 2)
410 (defconst context-coloring-SYMBOL-CODE 3)
411 (defconst context-coloring-OPEN-PARENTHESIS-CODE 4)
412 (defconst context-coloring-CLOSE-PARENTHESIS-CODE 5)
413 (defconst context-coloring-EXPRESSION-PREFIX-CODE 6)
414
415 (defconst context-coloring-APOSTROPHE-CHAR (string-to-char "'"))
416 (defconst context-coloring-OPEN-PARENTHESIS-CHAR (string-to-char "("))
417 (defconst context-coloring-COMMA-CHAR (string-to-char ","))
418 (defconst context-coloring-BACKTICK-CHAR (string-to-char "`"))
419
420 (defvar context-coloring-parse-interruptable-p t
421 "Set this to nil to force parse to continue until finished.")
422
423 (defconst context-coloring-emacs-lisp-iterations-per-pause 1000
424 "Pause after this many iterations to check for user input.
425 If user input is pending, stop the parse. This makes for a
426 smoother user experience for large files.
427
428 As of this writing, emacs lisp colorization seems to run at about
429 60,000 iterations per second. A default value of 1000 should
430 provide visually \"instant\" updates at 60 frames per second.")
431
432 (defvar context-coloring-elisp-scope-stack '())
433
434 (defsubst context-coloring-elisp-make-scope (level)
435 (list
436 :level level
437 :variables (make-hash-table :test 'equal)))
438
439 (defsubst context-coloring-elisp-scope-get-level (scope)
440 (plist-get scope :level))
441
442 (defsubst context-coloring-elisp-scope-add-variable (scope variable)
443 (puthash variable t (plist-get scope :variables)))
444
445 (defsubst context-coloring-elisp-scope-get-variable (scope variable)
446 (gethash variable (plist-get scope :variables)))
447
448 (defsubst context-coloring-elisp-get-variable-level (variable)
449 (let* ((scope-stack context-coloring-elisp-scope-stack)
450 scope
451 level)
452 (while (and scope-stack (not level))
453 (setq scope (car scope-stack))
454 (cond
455 ((context-coloring-elisp-scope-get-variable scope variable)
456 (setq level (context-coloring-elisp-scope-get-level scope)))
457 (t
458 (setq scope-stack (cdr scope-stack)))))
459 ;; Assume a global variable.
460 (or level 0)))
461
462 (defun context-coloring-elisp-push-scope ()
463 (push (context-coloring-elisp-make-scope
464 (1+ (context-coloring-elisp-current-scope-level)))
465 context-coloring-elisp-scope-stack))
466
467 (defun context-coloring-elisp-pop-scope ()
468 (pop context-coloring-elisp-scope-stack))
469
470 (defun context-coloring-elisp-add-variable (variable)
471 (let ((current-scope (car context-coloring-elisp-scope-stack)))
472 (context-coloring-elisp-scope-add-variable current-scope variable)))
473
474 (defun context-coloring-elisp-current-scope-level ()
475 (let ((current-scope (car context-coloring-elisp-scope-stack)))
476 (cond
477 (current-scope
478 (context-coloring-elisp-scope-get-level current-scope))
479 (t
480 0))))
481
482 (defun context-coloring-elisp-colorize-defun (&optional anonymous-p)
483 (let ((start (point))
484 end
485 stop
486 syntax
487 syntax-code
488 defun-name-pos
489 defun-name-end
490 arg-n-pos
491 arg-n-end
492 arg-n-string)
493 (context-coloring-elisp-push-scope)
494 ;; Color the whole sexp.
495 (forward-sexp)
496 (setq end (point))
497 (context-coloring-colorize-region
498 start
499 end
500 (context-coloring-elisp-current-scope-level))
501 (goto-char start)
502 ;; Skip past the "defun".
503 (skip-syntax-forward "^w_")
504 (forward-sexp)
505 (skip-syntax-forward " ")
506 (setq stop nil)
507 (unless anonymous-p
508 ;; Check for the defun's name.
509 (setq syntax (syntax-after (point)))
510 (setq syntax-code (syntax-class syntax))
511 (cond
512 ((or (= syntax-code context-coloring-WORD-CODE)
513 (= syntax-code context-coloring-SYMBOL-CODE))
514 ;; Color the defun's name with the top-level color.
515 (setq defun-name-pos (point))
516 (forward-sexp)
517 (setq defun-name-end (point))
518 (context-coloring-colorize-region defun-name-pos defun-name-end 0)
519 (skip-syntax-forward " "))
520 (t
521 (setq stop t))))
522 (cond
523 (stop
524 ;; Skip it.
525 (goto-char start)
526 (forward-sexp))
527 (t
528 (setq syntax (syntax-after (point)))
529 (setq syntax-code (syntax-class syntax))
530 (cond
531 ((= syntax-code context-coloring-OPEN-PARENTHESIS-CODE)
532 (forward-char)
533 (skip-syntax-forward " ")
534 (while (/= (progn
535 (setq syntax (syntax-after (point)))
536 (setq syntax-code (syntax-class syntax))
537 syntax-code)
538 context-coloring-CLOSE-PARENTHESIS-CODE)
539 (cond
540 ((or (= syntax-code context-coloring-WORD-CODE)
541 (= syntax-code context-coloring-SYMBOL-CODE))
542 (setq arg-n-pos (point))
543 (forward-sexp)
544 (setq arg-n-end (point))
545 (setq arg-n-string (buffer-substring-no-properties
546 arg-n-pos
547 arg-n-end))
548 (when (string-match-p
549 context-coloring-emacs-lisp-arglist-arg-regexp
550 arg-n-string)
551 (context-coloring-elisp-add-variable arg-n-string)))
552 (t
553 (forward-sexp)))
554 (skip-syntax-forward " "))
555 ;; Skip the closing arglist paren.
556 (forward-char)
557 ;; Colorize the rest of the function.
558 (context-coloring-elisp-colorize-region (point) (1- end))
559 ;; Exit the defun.
560 (forward-char))
561 (t
562 ;; Skip it.
563 (goto-char start)
564 (forward-sexp)))))
565 (context-coloring-elisp-pop-scope)))
566
567 (defun context-coloring-elisp-colorize-lambda ()
568 (context-coloring-elisp-colorize-defun t))
569
570 (defun context-coloring-elisp-colorize-parenthesized-sexp ()
571 (let ((start (point))
572 end
573 syntax
574 syntax-code
575 child-0-pos
576 child-0-end
577 child-0-string)
578 (forward-sexp)
579 (setq end (point))
580 (goto-char start)
581 (forward-char)
582 (skip-syntax-forward " ")
583 (setq syntax (syntax-after (point)))
584 (setq syntax-code (syntax-class syntax))
585 ;; Figure out if the sexp is a special form.
586 (cond
587 ((or (= syntax-code context-coloring-WORD-CODE)
588 (= syntax-code context-coloring-SYMBOL-CODE))
589 (setq child-0-pos (point))
590 (forward-sexp)
591 (setq child-0-end (point))
592 (setq child-0-string (buffer-substring-no-properties
593 child-0-pos
594 child-0-end))
595 (cond
596 ((string-match-p context-coloring-emacs-lisp-defun-regexp child-0-string)
597 (goto-char start)
598 (context-coloring-elisp-colorize-defun))
599 ((string-match-p context-coloring-emacs-lisp-lambda-regexp child-0-string)
600 (goto-char start)
601 (context-coloring-elisp-colorize-lambda))
602 ;; Not a special form; just colorize the remaining region.
603 (t
604 (context-coloring-colorize-region
605 start
606 end
607 (context-coloring-elisp-current-scope-level))
608 (context-coloring-elisp-colorize-region (point) (1- end))
609 (forward-char))))
610 (t
611 ;; Skip it.
612 (goto-char start)
613 (forward-sexp)))))
614
615 (defun context-coloring-elisp-colorize-symbol ()
616 (let (symbol-pos
617 symbol-end
618 symbol-string)
619 (setq symbol-pos (point))
620 (forward-sexp)
621 (setq symbol-end (point))
622 (setq symbol-string (buffer-substring-no-properties
623 symbol-pos
624 symbol-end))
625 (cond
626 ((string-match-p context-coloring-ignored-word-regexp symbol-string))
627 (t
628 (context-coloring-colorize-region
629 symbol-pos
630 symbol-end
631 (context-coloring-elisp-get-variable-level
632 (buffer-substring-no-properties
633 symbol-pos
634 symbol-end)))))))
635
636 (defun context-coloring-elisp-colorize-expression-prefix ()
637 (let (start
638 end
639 char)
640 (setq char (char-after))
641 (cond
642 ((= char context-coloring-APOSTROPHE-CHAR)
643 (forward-sexp))
644 ((= char context-coloring-BACKTICK-CHAR)
645 (setq start (point))
646 (forward-sexp)
647 (setq end (point))
648 (goto-char start)
649 (while (> end (progn (forward-char)
650 (point)))
651 (setq char (char-after))
652 (when (= char context-coloring-COMMA-CHAR)
653 (forward-char)
654 (skip-syntax-forward " ")
655 (context-coloring-elisp-colorize-sexp)))))))
656
657 (defun context-coloring-elisp-colorize-sexp ()
658 (let (syntax
659 syntax-code)
660 (setq syntax (syntax-after (point)))
661 (setq syntax-code (syntax-class syntax))
662 (cond
663 ((= syntax-code context-coloring-OPEN-PARENTHESIS-CODE)
664 (context-coloring-elisp-colorize-parenthesized-sexp))
665 ((or (= syntax-code context-coloring-WORD-CODE)
666 (= syntax-code context-coloring-SYMBOL-CODE))
667 (context-coloring-elisp-colorize-symbol))
668 ((= syntax-code context-coloring-EXPRESSION-PREFIX-CODE)
669 (context-coloring-elisp-colorize-expression-prefix))
670 (t
671 (forward-char)))))
672
673 (defun context-coloring-elisp-colorize-region (start end)
674 (let (syntax
675 syntax-code)
676 (goto-char start)
677 (while (> end (progn (skip-syntax-forward "^()w_'" end)
678 (point)))
679 (setq syntax (syntax-after (point)))
680 (setq syntax-code (syntax-class syntax))
681 (cond
682 ((or (= syntax-code context-coloring-OPEN-PARENTHESIS-CODE)
683 (= syntax-code context-coloring-WORD-CODE)
684 (= syntax-code context-coloring-SYMBOL-CODE)
685 (= syntax-code context-coloring-EXPRESSION-PREFIX-CODE))
686 (context-coloring-elisp-colorize-sexp))
687 (t
688 (forward-char))))))
689
690 (defun context-coloring-elisp-colorize-changed-region (start end)
691 (with-silent-modifications
692 (save-excursion
693 (let ((start (progn (goto-char start)
694 (beginning-of-defun)
695 (point)))
696 (end (progn (goto-char end)
697 (end-of-defun)
698 (point))))
699 (setq context-coloring-elisp-scope-stack '())
700 (context-coloring-elisp-colorize-region start end)))))
701
702 (defun context-coloring-elisp-colorize-buffer ()
703 (interactive)
704 (with-silent-modifications
705 (save-excursion
706 (setq context-coloring-elisp-scope-stack '())
707 (context-coloring-elisp-colorize-region (point-min) (point-max)))))
708
709 (defalias 'ccecb 'context-coloring-elisp-colorize-buffer)
710
711 ;; TODO: Add cases for special forms like `cond'.
712 ;; TODO: Backticks only go one level deep.
713 ;; TODO: Refactor this function into smaller, focused ones so we can parse
714 ;; recursively and easily.
715 (defun context-coloring-emacs-lisp-colorize ()
716 "Color the current buffer by parsing emacs lisp sexps."
717 (with-silent-modifications
718 (save-excursion
719 ;; TODO: Can probably make this lazy to the nearest defun.
720 (goto-char (point-min))
721 (let* ((inhibit-point-motion-hooks t)
722 (end (point-max))
723 (iteration-count 0)
724 (last-fontified-position (point))
725 beginning-of-current-defun
726 end-of-current-defun
727 (last-ppss-pos (point))
728 (ppss (syntax-ppss))
729 ppss-depth
730 ;; -1 never matches a depth. This is a minor optimization.
731 (scope-stack `(,(context-coloring-make-scope -1 0)))
732 (backtick-stack '())
733 (let-varlist-stack '())
734 (let-var-stack '())
735 popped-vars
736 one-word-found-p
737 in-defun-p
738 in-lambda-p
739 in-let-p
740 in-let*-p
741 defun-arglist
742 defun-arg
743 let-varlist
744 let-varlist-type
745 variable
746 variable-end
747 variable-string
748 variable-scope-level
749 token-pos
750 token-syntax
751 token-syntax-code
752 token-char
753 child-0-pos
754 child-0-end
755 child-0-syntax
756 child-0-syntax-code
757 child-0-string
758 child-1-pos
759 child-1-end
760 child-1-syntax
761 child-1-syntax-code
762 child-2-end)
763 (while (> end (progn (skip-syntax-forward "^()w_'" end)
764 (point)))
765 ;; Sparingly-executed tasks.
766 (setq iteration-count (1+ iteration-count))
767 (when (zerop (% iteration-count
768 context-coloring-emacs-lisp-iterations-per-pause))
769 ;; Fontify until the end of the current defun because doing it in
770 ;; chunks based soley on point could result in partial
771 ;; re-fontifications over the contents of scopes.
772 (save-excursion
773 (end-of-defun)
774 (setq end-of-current-defun (point))
775 (beginning-of-defun)
776 (setq beginning-of-current-defun (point)))
777
778 ;; Fontify in chunks.
779 (context-coloring-maybe-colorize-comments-and-strings
780 last-fontified-position
781 (cond
782 ;; We weren't actually in a defun, so don't color the next one, as
783 ;; that could result in `font-lock' properties being added to it.
784 ((> beginning-of-current-defun (point))
785 (point))
786 (t
787 end-of-current-defun)))
788 (setq last-fontified-position (point))
789 (when (and context-coloring-parse-interruptable-p
790 (input-pending-p))
791 (throw 'interrupted t)))
792
793 (setq token-pos (point))
794 (setq token-syntax (syntax-after token-pos))
795 (setq token-syntax-code (logand #xFFFF (car token-syntax)))
796 (setq token-char (char-after))
797 (setq ppss (parse-partial-sexp last-ppss-pos token-pos nil nil ppss))
798 (setq last-ppss-pos token-pos)
799 (cond
800
801 ;; Resolve an invalid state.
802 ((cond
803 ;; Inside string?
804 ((nth 3 ppss)
805 (skip-syntax-forward "^\"" end)
806 (forward-char)
807 t)
808 ;; Inside comment?
809 ((nth 4 ppss)
810 (skip-syntax-forward "^>" end)
811 t)))
812
813 ;; Need to check early in case there's a comma.
814 ((context-coloring-expression-prefix-p token-syntax-code)
815 (forward-char)
816 (cond
817 ;; Skip top-level symbols.
818 ((not (or backtick-stack
819 (= token-char context-coloring-BACKTICK-CHAR)))
820 (goto-char (context-coloring-forward-sexp-position)))
821 ;; Push a backtick state.
822 ((or (= token-char context-coloring-BACKTICK-CHAR)
823 (= token-char context-coloring-COMMA-CHAR))
824 (setq backtick-stack (cons (context-coloring-make-backtick
825 (context-coloring-forward-sexp-position)
826 (= token-char context-coloring-BACKTICK-CHAR))
827 backtick-stack)))))
828
829 ;; Pop a backtick state.
830 ((and backtick-stack
831 (>= (point) (context-coloring-backtick-get-end (car backtick-stack))))
832 (setq backtick-stack (cdr backtick-stack)))
833
834 ;; Restricted by an enabled backtick.
835 ((and backtick-stack
836 (context-coloring-backtick-enabled-p backtick-stack))
837 (forward-char))
838
839 ((context-coloring-open-parenthesis-p token-syntax-code)
840 (forward-char)
841 ;; Look for function calls.
842 (context-coloring-forward-sws)
843 (setq child-0-pos (point))
844 (setq child-0-syntax (syntax-after child-0-pos))
845 (setq child-0-syntax-code (logand #xFFFF (car child-0-syntax)))
846 (cond
847 ((context-coloring-emacs-lisp-identifier-syntax-p child-0-syntax-code)
848 (setq one-word-found-p t)
849 (setq child-0-end (scan-sexps child-0-pos 1))
850 (setq child-0-string (buffer-substring-no-properties child-0-pos child-0-end))
851 (cond
852 ;; Parse a var in a `let' varlist.
853 ((and
854 let-varlist-stack
855 (context-coloring-at-stack-depth-p
856 let-varlist-stack
857 ;; 1- because we're inside the varlist.
858 (1- (context-coloring-ppss-depth ppss))))
859 (context-coloring-let-varlist-add-var
860 (car let-varlist-stack)
861 (intern child-0-string))
862 (setq let-var-stack (cons (context-coloring-ppss-depth ppss)
863 let-var-stack)))
864 ((string-match-p context-coloring-emacs-lisp-defun-regexp child-0-string)
865 (setq in-defun-p t))
866 ((string-match-p context-coloring-emacs-lisp-lambda-regexp child-0-string)
867 (setq in-lambda-p t))
868 ((string-match-p context-coloring-emacs-lisp-let-regexp child-0-string)
869 (setq in-let-p t)
870 (setq let-varlist-type 'let))
871 ((string-match-p context-coloring-emacs-lisp-let*-regexp child-0-string)
872 (setq in-let*-p t)
873 (setq let-varlist-type 'let*)))))
874 (when (or in-defun-p
875 in-lambda-p
876 in-let-p
877 in-let*-p)
878 (setq scope-stack (cons (context-coloring-make-scope
879 (context-coloring-ppss-depth ppss)
880 (1+ (context-coloring-scope-get-level
881 (car scope-stack))))
882 scope-stack)))
883 ;; TODO: Maybe wasteful but doing this conditionally doesn't make
884 ;; much of a difference.
885 (context-coloring-colorize-region token-pos
886 (scan-sexps token-pos 1)
887 (context-coloring-scope-get-level
888 (car scope-stack)))
889 (cond
890 ((or in-defun-p
891 in-lambda-p)
892 (goto-char child-0-end)
893 (when in-defun-p
894 ;; Look for a function name.
895 (context-coloring-forward-sws)
896 (setq child-1-pos (point))
897 (setq child-1-syntax (syntax-after child-1-pos))
898 (setq child-1-syntax-code (logand #xFFFF (car child-1-syntax)))
899 (cond
900 ((context-coloring-emacs-lisp-identifier-syntax-p child-1-syntax-code)
901 (setq child-1-end (scan-sexps child-1-pos 1))
902 ;; Defuns are global, so use level 0.
903 (context-coloring-colorize-region child-1-pos child-1-end 0)
904 (goto-char child-1-end))))
905 ;; Look for an arglist.
906 (context-coloring-forward-sws)
907 (when (context-coloring-at-open-parenthesis-p)
908 ;; (Actually it should be `child-1-end' for `lambda'.)
909 (setq child-2-end (context-coloring-forward-sexp-position))
910 (setq defun-arglist (read (buffer-substring-no-properties
911 (point)
912 child-2-end)))
913 (while defun-arglist
914 (setq defun-arg (car defun-arglist))
915 (when (and (symbolp defun-arg)
916 (string-match-p
917 context-coloring-emacs-lisp-arglist-arg-regexp
918 (symbol-name defun-arg)))
919 (context-coloring-scope-add-variable
920 (car scope-stack)
921 defun-arg))
922 (setq defun-arglist (cdr defun-arglist)))
923 (goto-char child-2-end))
924 ;; Cleanup.
925 (setq in-defun-p nil)
926 (setq in-lambda-p nil))
927 ((or in-let-p
928 in-let*-p)
929 (goto-char child-0-end)
930 ;; Look for a varlist.
931 (context-coloring-forward-sws)
932 (setq child-1-pos (point))
933 (setq child-1-syntax (syntax-after child-1-pos))
934 (setq child-1-syntax-code (logand #xFFFF (car child-1-syntax)))
935 (when (context-coloring-open-parenthesis-p child-1-syntax-code)
936 ;; Begin parsing the varlist.
937 (forward-char)
938 (setq let-varlist-stack (cons (context-coloring-make-let-varlist
939 ;; 1+ because we parsed it at a
940 ;; higher depth.
941 (1+ (context-coloring-ppss-depth ppss))
942 let-varlist-type)
943 let-varlist-stack)))
944 ;; Cleanup.
945 (setq in-let-p nil)
946 (setq in-let*-p nil))
947 (t
948 (goto-char (cond
949 ;; If there was a word, continue parsing after it.
950 (one-word-found-p
951 (1+ child-0-end))
952 (t
953 (1+ token-pos))))))
954 ;; Cleanup.
955 (setq one-word-found-p nil))
956
957 ((context-coloring-emacs-lisp-identifier-syntax-p token-syntax-code)
958 (setq variable-end (context-coloring-forward-sexp-position))
959 (setq variable-string (buffer-substring-no-properties
960 token-pos
961 variable-end))
962 (cond
963 ;; Ignore constants such as numbers, keywords, t, nil. These can't
964 ;; be rebound, so they should be treated like syntax.
965 ((string-match-p context-coloring-ignored-word-regexp variable-string))
966 ((keywordp (read variable-string)))
967 (t
968 (setq variable (intern variable-string))
969 (cond
970 ;; Parse a `let' varlist's uninitialized var.
971 ((and
972 let-varlist-stack
973 (context-coloring-at-stack-depth-p
974 let-varlist-stack
975 ;; 1- because we're inside the varlist.
976 (1- (context-coloring-ppss-depth ppss))))
977 (setq let-varlist (car let-varlist-stack))
978 (setq let-varlist-type (context-coloring-let-varlist-get-type let-varlist))
979 (cond
980 ;; Defer `let' binding until the end of the varlist.
981 ((eq let-varlist-type 'let)
982 (context-coloring-let-varlist-add-var let-varlist variable))
983 ;; Bind a `let*' right away.
984 ((eq let-varlist-type 'let*)
985 (context-coloring-scope-add-variable (car scope-stack) variable))))
986 (t
987 (setq variable-scope-level
988 (context-coloring-get-variable-level scope-stack variable))
989 (when (/= variable-scope-level (context-coloring-scope-get-level
990 (car scope-stack)))
991 (context-coloring-colorize-region
992 token-pos
993 variable-end
994 variable-scope-level))))))
995 (goto-char variable-end))
996
997 ((context-coloring-close-parenthesis-p token-syntax-code)
998 (forward-char)
999 (setq ppss (parse-partial-sexp last-ppss-pos (point) nil nil ppss))
1000 (setq last-ppss-pos (point))
1001 (setq ppss-depth (context-coloring-ppss-depth ppss))
1002 ;; TODO: Order might matter here but I'm not certain.
1003 (when (context-coloring-at-stack-depth-p scope-stack ppss-depth)
1004 (setq scope-stack (cdr scope-stack)))
1005 (when (and
1006 let-var-stack
1007 (= (car let-var-stack) ppss-depth))
1008 (setq let-var-stack (cdr let-var-stack))
1009 (when (eq (context-coloring-let-varlist-get-type (car let-varlist-stack))
1010 'let*)
1011 (setq popped-vars (context-coloring-let-varlist-pop-vars
1012 (car let-varlist-stack)))))
1013 (when (and
1014 let-varlist-stack
1015 (context-coloring-at-stack-depth-p let-varlist-stack ppss-depth))
1016 (setq popped-vars (context-coloring-let-varlist-pop-vars
1017 (car let-varlist-stack)))
1018 (setq let-varlist-stack (cdr let-varlist-stack)))
1019 (while popped-vars
1020 (context-coloring-scope-add-variable (car scope-stack) (car popped-vars))
1021 (setq popped-vars (cdr popped-vars))))
1022
1023 ))
1024 ;; Fontify the last stretch.
1025 (context-coloring-maybe-colorize-comments-and-strings
1026 last-fontified-position
1027 (point))))))
1028
1029
1030 ;;; Shell command scopification / colorization
1031
1032 (defun context-coloring-apply-tokens (tokens)
1033 "Process a vector of TOKENS to apply context-based coloring to
1034 the current buffer. Tokens are 3 integers: start, end, level.
1035 The vector is flat, with a new token occurring after every 3rd
1036 element."
1037 (with-silent-modifications
1038 (let ((i 0)
1039 (len (length tokens)))
1040 (while (< i len)
1041 (context-coloring-colorize-region
1042 (elt tokens i)
1043 (elt tokens (+ i 1))
1044 (elt tokens (+ i 2)))
1045 (setq i (+ i 3))))
1046 (context-coloring-maybe-colorize-comments-and-strings)))
1047
1048 (defun context-coloring-parse-array (array)
1049 "Parse ARRAY as a flat JSON array of numbers."
1050 (let ((braceless (substring (context-coloring-trim array) 1 -1)))
1051 (cond
1052 ((> (length braceless) 0)
1053 (vconcat
1054 (mapcar 'string-to-number (split-string braceless ","))))
1055 (t
1056 (vector)))))
1057
1058 (defvar-local context-coloring-scopifier-process nil
1059 "The single scopifier process that can be running.")
1060
1061 (defun context-coloring-kill-scopifier ()
1062 "Kill the currently-running scopifier process."
1063 (when (not (null context-coloring-scopifier-process))
1064 (delete-process context-coloring-scopifier-process)
1065 (setq context-coloring-scopifier-process nil)))
1066
1067 (defun context-coloring-scopify-shell-command (command callback)
1068 "Invoke a scopifier via COMMAND, read its response
1069 asynchronously and invoke CALLBACK with its output."
1070
1071 ;; Prior running tokenization is implicitly obsolete if this function is
1072 ;; called.
1073 (context-coloring-kill-scopifier)
1074
1075 ;; Start the process.
1076 (setq context-coloring-scopifier-process
1077 (start-process-shell-command "scopifier" nil command))
1078
1079 (let ((output ""))
1080
1081 ;; The process may produce output in multiple chunks. This filter
1082 ;; accumulates the chunks into a message.
1083 (set-process-filter
1084 context-coloring-scopifier-process
1085 (lambda (_process chunk)
1086 (setq output (concat output chunk))))
1087
1088 ;; When the process's message is complete, this sentinel parses it as JSON
1089 ;; and applies the tokens to the buffer.
1090 (set-process-sentinel
1091 context-coloring-scopifier-process
1092 (lambda (_process event)
1093 (when (equal "finished\n" event)
1094 (funcall callback output))))))
1095
1096 (defun context-coloring-send-buffer-to-scopifier ()
1097 "Give the scopifier process its input so it can begin
1098 scopifying."
1099 (process-send-region
1100 context-coloring-scopifier-process
1101 (point-min) (point-max))
1102 (process-send-eof
1103 context-coloring-scopifier-process))
1104
1105 (defun context-coloring-scopify-and-colorize (command &optional callback)
1106 "Invoke a scopifier via COMMAND with the current buffer's contents,
1107 read the scopifier's response asynchronously and apply a parsed
1108 list of tokens to `context-coloring-apply-tokens'.
1109
1110 Invoke CALLBACK when complete."
1111 (let ((buffer (current-buffer)))
1112 (context-coloring-scopify-shell-command
1113 command
1114 (lambda (output)
1115 (let ((tokens (context-coloring-parse-array output)))
1116 (with-current-buffer buffer
1117 (context-coloring-apply-tokens tokens))
1118 (setq context-coloring-scopifier-process nil)
1119 (when callback (funcall callback))))))
1120 (context-coloring-send-buffer-to-scopifier))
1121
1122
1123 ;;; Dispatch
1124
1125 (defvar context-coloring-dispatch-hash-table (make-hash-table :test 'eq)
1126 "Map dispatch strategy names to their corresponding property
1127 lists, which contain details about the strategies.")
1128
1129 (defvar context-coloring-mode-hash-table (make-hash-table :test 'eq)
1130 "Map major mode names to dispatch property lists.")
1131
1132 (defun context-coloring-get-dispatch-for-mode (mode)
1133 "Return the dispatch for MODE (or a derivative mode)."
1134 (let ((parent mode)
1135 dispatch)
1136 (while (and parent
1137 (not (setq dispatch (gethash parent context-coloring-mode-hash-table)))
1138 (setq parent (get parent 'derived-mode-parent))))
1139 dispatch))
1140
1141 (defun context-coloring-define-dispatch (symbol &rest properties)
1142 "Define a new dispatch named SYMBOL with PROPERTIES.
1143
1144 A \"dispatch\" is a property list describing a strategy for
1145 coloring a buffer. There are three possible strategies: Parse
1146 and color in a single function (`:colorizer'), parse in a
1147 function that returns scope data (`:scopifier'), or parse with a
1148 shell command that returns scope data (`:command'). In the
1149 latter two cases, the scope data will be used to automatically
1150 color the buffer.
1151
1152 PROPERTIES must include `:modes' and one of `:colorizer',
1153 `:scopifier' or `:command'.
1154
1155 `:modes' - List of major modes this dispatch is valid for.
1156
1157 `:colorizer' - Symbol referring to a function that parses and
1158 colors the buffer.
1159
1160 `:scopifier' - Symbol referring to a function that parses the
1161 buffer a returns a flat vector of start, end and level data.
1162
1163 `:executable' - Optional name of an executable required by
1164 `:command'.
1165
1166 `:command' - Shell command to execute with the current buffer
1167 sent via stdin, and with a flat JSON array of start, end and
1168 level data returned via stdout.
1169
1170 `:version' - Minimum required version that should be printed when
1171 executing `:command' with a \"--version\" flag. The version
1172 should be numeric, e.g. \"2\", \"19700101\", \"1.2.3\",
1173 \"v1.2.3\" etc.
1174
1175 `:setup' - Arbitrary code to set up this dispatch when
1176 `context-coloring-mode' is enabled.
1177
1178 `:teardown' - Arbitrary code to tear down this dispatch when
1179 `context-coloring-mode' is disabled."
1180 (let ((modes (plist-get properties :modes))
1181 (colorizer (plist-get properties :colorizer))
1182 (scopifier (plist-get properties :scopifier))
1183 (command (plist-get properties :command)))
1184 (when (null modes)
1185 (error "No mode defined for dispatch"))
1186 (when (not (or colorizer
1187 scopifier
1188 command))
1189 (error "No colorizer, scopifier or command defined for dispatch"))
1190 (puthash symbol properties context-coloring-dispatch-hash-table)
1191 (dolist (mode modes)
1192 (puthash mode properties context-coloring-mode-hash-table))))
1193
1194
1195 ;;; Colorization
1196
1197 (defvar context-coloring-colorize-hook nil
1198 "Hooks to run after coloring a buffer.")
1199
1200 (defun context-coloring-colorize (&optional callback)
1201 "Color the current buffer by function context.
1202
1203 Invoke CALLBACK when complete; see `context-coloring-dispatch'."
1204 (interactive)
1205 (context-coloring-dispatch
1206 (lambda ()
1207 (when callback (funcall callback))
1208 (run-hooks 'context-coloring-colorize-hook))))
1209
1210 (defvar-local context-coloring-changed nil
1211 "Indication that the buffer has changed recently, which implies
1212 that it should be colored again by
1213 `context-coloring-colorize-idle-timer' if that timer is being
1214 used.")
1215
1216 (defun context-coloring-change-function (_start _end _length)
1217 "Register a change so that a buffer can be colorized soon."
1218 ;; Tokenization is obsolete if there was a change.
1219 (context-coloring-kill-scopifier)
1220 (setq context-coloring-changed t))
1221
1222 (defun context-coloring-maybe-colorize (buffer)
1223 "Colorize the current buffer if it has changed."
1224 (when (and (eq buffer (current-buffer))
1225 context-coloring-changed)
1226 (setq context-coloring-changed nil)
1227 (context-coloring-colorize)))
1228
1229
1230 ;;; Versioning
1231
1232 (defun context-coloring-parse-version (string)
1233 "Extract segments of a version STRING into a list. \"v1.0.0\"
1234 produces (1 0 0), \"19700101\" produces (19700101), etc."
1235 (let (version)
1236 (while (string-match "[0-9]+" string)
1237 (setq version (append version
1238 (list (string-to-number (match-string 0 string)))))
1239 (setq string (substring string (match-end 0))))
1240 version))
1241
1242 (defun context-coloring-check-version (expected actual)
1243 "Check that version EXPECTED is less than or equal to ACTUAL."
1244 (let ((expected (context-coloring-parse-version expected))
1245 (actual (context-coloring-parse-version actual))
1246 (continue t)
1247 (acceptable t))
1248 (while (and continue expected)
1249 (let ((an-expected (car expected))
1250 (an-actual (car actual)))
1251 (cond
1252 ((> an-actual an-expected)
1253 (setq acceptable t)
1254 (setq continue nil))
1255 ((< an-actual an-expected)
1256 (setq acceptable nil)
1257 (setq continue nil))))
1258 (setq expected (cdr expected))
1259 (setq actual (cdr actual)))
1260 acceptable))
1261
1262 (defvar context-coloring-check-scopifier-version-hook nil
1263 "Hooks to run after checking the scopifier version.")
1264
1265 (defun context-coloring-check-scopifier-version (&optional callback)
1266 "Asynchronously invoke CALLBACK with a predicate indicating
1267 whether the current scopifier version satisfies the minimum
1268 version number required for the current major mode."
1269 (let ((dispatch (context-coloring-get-dispatch-for-mode major-mode)))
1270 (when dispatch
1271 (let ((version (plist-get dispatch :version))
1272 (command (plist-get dispatch :command)))
1273 (context-coloring-scopify-shell-command
1274 (context-coloring-join (list command "--version") " ")
1275 (lambda (output)
1276 (if (context-coloring-check-version version output)
1277 (progn
1278 (when callback (funcall callback t)))
1279 (when callback (funcall callback nil)))
1280 (run-hooks 'context-coloring-check-scopifier-version-hook)))))))
1281
1282
1283 ;;; Themes
1284
1285 (defvar context-coloring-theme-hash-table (make-hash-table :test 'eq)
1286 "Map theme names to theme properties.")
1287
1288 (defun context-coloring-theme-p (theme)
1289 "Return t if THEME is defined, nil otherwise."
1290 (and (gethash theme context-coloring-theme-hash-table)))
1291
1292 (defconst context-coloring-level-face-regexp
1293 "context-coloring-level-\\([[:digit:]]+\\)-face"
1294 "Extract a level from a face.")
1295
1296 (defvar context-coloring-originally-set-theme-hash-table
1297 (make-hash-table :test 'eq)
1298 "Cache custom themes who originally set their own
1299 `context-coloring-level-N-face' faces.")
1300
1301 (defun context-coloring-theme-originally-set-p (theme)
1302 "Return t if there is a `context-coloring-level-N-face'
1303 originally set for THEME, nil otherwise."
1304 (let (originally-set)
1305 (cond
1306 ;; `setq' might return a non-nil value for the sake of this `cond'.
1307 ((setq
1308 originally-set
1309 (gethash
1310 theme
1311 context-coloring-originally-set-theme-hash-table))
1312 (eq originally-set 'yes))
1313 (t
1314 (let* ((settings (get theme 'theme-settings))
1315 (tail settings)
1316 found)
1317 (while (and tail (not found))
1318 (and (eq (nth 0 (car tail)) 'theme-face)
1319 (string-match
1320 context-coloring-level-face-regexp
1321 (symbol-name (nth 1 (car tail))))
1322 (setq found t))
1323 (setq tail (cdr tail)))
1324 found)))))
1325
1326 (defun context-coloring-cache-originally-set (theme originally-set)
1327 "Remember if THEME had colors originally set for it. If
1328 ORIGINALLY-SET is non-nil, it did, otherwise it didn't."
1329 ;; Caching whether a theme was originally set is kind of dirty, but we have to
1330 ;; do it to remember the past state of the theme. There are probably some
1331 ;; edge cases where caching will be an issue, but they are probably rare.
1332 (puthash
1333 theme
1334 (if originally-set 'yes 'no)
1335 context-coloring-originally-set-theme-hash-table))
1336
1337 (defun context-coloring-warn-theme-originally-set (theme)
1338 "Warn the user that the colors for THEME are already originally
1339 set."
1340 (warn "Context coloring colors for theme `%s' are already defined" theme))
1341
1342 (defun context-coloring-theme-highest-level (theme)
1343 "Return the highest level N of a face like
1344 `context-coloring-level-N-face' set for THEME, or `-1' if there
1345 is none."
1346 (let* ((settings (get theme 'theme-settings))
1347 (tail settings)
1348 face-string
1349 number
1350 (found -1))
1351 (while tail
1352 (and (eq (nth 0 (car tail)) 'theme-face)
1353 (setq face-string (symbol-name (nth 1 (car tail))))
1354 (string-match
1355 context-coloring-level-face-regexp
1356 face-string)
1357 (setq number (string-to-number
1358 (substring face-string
1359 (match-beginning 1)
1360 (match-end 1))))
1361 (> number found)
1362 (setq found number))
1363 (setq tail (cdr tail)))
1364 found))
1365
1366 (defun context-coloring-apply-theme (theme)
1367 "Apply THEME's properties to its respective custom theme,
1368 which must already exist and which *should* already be enabled."
1369 (let* ((properties (gethash theme context-coloring-theme-hash-table))
1370 (colors (plist-get properties :colors))
1371 (level -1))
1372 ;; Only clobber when we have to.
1373 (when (custom-theme-enabled-p theme)
1374 (setq context-coloring-maximum-face (- (length colors) 1)))
1375 (apply
1376 'custom-theme-set-faces
1377 theme
1378 (mapcar
1379 (lambda (color)
1380 (setq level (+ level 1))
1381 `(,(context-coloring-level-face level) ((t (:foreground ,color)))))
1382 colors))))
1383
1384 (defun context-coloring-define-theme (theme &rest properties)
1385 "Define a context theme named THEME for coloring scope levels.
1386
1387 PROPERTIES is a property list specifiying the following details:
1388
1389 `:aliases': List of symbols of other custom themes that these
1390 colors are applicable to.
1391
1392 `:colors': List of colors that this context theme uses.
1393
1394 `:override': If non-nil, this context theme is intentionally
1395 overriding colors set by a custom theme. Don't set this non-nil
1396 unless there is a custom theme you want to use which sets
1397 `context-coloring-level-N-face' faces that you want to replace.
1398
1399 `:recede': If non-nil, this context theme should not apply its
1400 colors if a custom theme already sets
1401 `context-coloring-level-N-face' faces. This option is
1402 optimistic; set this non-nil if you would rather confer the duty
1403 of picking colors to a custom theme author (if / when he ever
1404 gets around to it).
1405
1406 By default, context themes will always override custom themes,
1407 even if those custom themes set `context-coloring-level-N-face'
1408 faces. If a context theme does override a custom theme, a
1409 warning will be raised, at which point you may want to enable the
1410 `:override' option, or just delete your context theme and opt to
1411 use your custom theme's author's colors instead.
1412
1413 Context themes only work for the custom theme with the highest
1414 precedence, i.e. the car of `custom-enabled-themes'."
1415 (let ((aliases (plist-get properties :aliases))
1416 (override (plist-get properties :override))
1417 (recede (plist-get properties :recede)))
1418 (dolist (name (append `(,theme) aliases))
1419 (puthash name properties context-coloring-theme-hash-table)
1420 (when (custom-theme-p name)
1421 (let ((originally-set (context-coloring-theme-originally-set-p name)))
1422 (context-coloring-cache-originally-set name originally-set)
1423 ;; In the particular case when you innocently define colors that a
1424 ;; custom theme originally set, warn. Arguably this only has to be
1425 ;; done at enable time, but it is probably more useful to do it at
1426 ;; definition time for prompter feedback.
1427 (when (and originally-set
1428 (not recede)
1429 (not override))
1430 (context-coloring-warn-theme-originally-set name))
1431 ;; Set (or overwrite) colors.
1432 (when (not (and originally-set
1433 recede))
1434 (context-coloring-apply-theme name)))))))
1435
1436 (defun context-coloring-enable-theme (theme)
1437 "Apply THEME if its colors are not already set, else just set
1438 `context-coloring-maximum-face' to the correct value for THEME."
1439 (let* ((properties (gethash theme context-coloring-theme-hash-table))
1440 (recede (plist-get properties :recede))
1441 (override (plist-get properties :override)))
1442 (cond
1443 (recede
1444 (let ((highest-level (context-coloring-theme-highest-level theme)))
1445 (cond
1446 ;; This can be true whether originally set by a custom theme or by a
1447 ;; context theme.
1448 ((> highest-level -1)
1449 (setq context-coloring-maximum-face highest-level))
1450 ;; It is possible that the corresponding custom theme did not exist at
1451 ;; the time of defining this context theme, and in that case the above
1452 ;; condition proves the custom theme did not originally set any faces,
1453 ;; so we have license to apply the context theme for the first time
1454 ;; here.
1455 (t
1456 (context-coloring-apply-theme theme)))))
1457 (t
1458 (let ((originally-set (context-coloring-theme-originally-set-p theme)))
1459 ;; Cache now in case the context theme was defined after the custom
1460 ;; theme.
1461 (context-coloring-cache-originally-set theme originally-set)
1462 (when (and originally-set
1463 (not override))
1464 (context-coloring-warn-theme-originally-set theme))
1465 (context-coloring-apply-theme theme))))))
1466
1467 (defadvice enable-theme (after context-coloring-enable-theme (theme) activate)
1468 "Enable colors for context themes just-in-time."
1469 (when (and (not (eq theme 'user)) ; Called internally by `enable-theme'.
1470 (custom-theme-p theme) ; Guard against non-existent themes.
1471 (context-coloring-theme-p theme))
1472 (when (= (length custom-enabled-themes) 1)
1473 ;; Cache because we can't reliably figure it out in reverse.
1474 (setq context-coloring-original-maximum-face
1475 context-coloring-maximum-face))
1476 (context-coloring-enable-theme theme)))
1477
1478 (defadvice disable-theme (after context-coloring-disable-theme (theme) activate)
1479 "Update `context-coloring-maximum-face'."
1480 (when (custom-theme-p theme) ; Guard against non-existent themes.
1481 (let ((enabled-theme (car custom-enabled-themes)))
1482 (if (context-coloring-theme-p enabled-theme)
1483 (progn
1484 (context-coloring-enable-theme enabled-theme))
1485 ;; Assume we are back to no theme; act as if nothing ever happened.
1486 ;; This is still prone to intervention, but rather extraordinarily.
1487 (setq context-coloring-maximum-face
1488 context-coloring-original-maximum-face)))))
1489
1490 (context-coloring-define-theme
1491 'ample
1492 :recede t
1493 :colors '("#bdbdb3"
1494 "#baba36"
1495 "#6aaf50"
1496 "#5180b3"
1497 "#ab75c3"
1498 "#cd7542"
1499 "#df9522"
1500 "#454545"))
1501
1502 (context-coloring-define-theme
1503 'anti-zenburn
1504 :recede t
1505 :colors '("#232333"
1506 "#6c1f1c"
1507 "#401440"
1508 "#0f2050"
1509 "#205070"
1510 "#336c6c"
1511 "#23733c"
1512 "#6b400c"
1513 "#603a60"
1514 "#2f4070"
1515 "#235c5c"))
1516
1517 (context-coloring-define-theme
1518 'grandshell
1519 :recede t
1520 :colors '("#bebebe"
1521 "#5af2ee"
1522 "#b2baf6"
1523 "#f09fff"
1524 "#efc334"
1525 "#f6df92"
1526 "#acfb5a"
1527 "#888888"))
1528
1529 (context-coloring-define-theme
1530 'leuven
1531 :recede t
1532 :colors '("#333333"
1533 "#0000ff"
1534 "#6434a3"
1535 "#ba36a5"
1536 "#d0372d"
1537 "#036a07"
1538 "#006699"
1539 "#006fe0"
1540 "#808080"))
1541
1542 (context-coloring-define-theme
1543 'monokai
1544 :recede t
1545 :colors '("#f8f8f2"
1546 "#66d9ef"
1547 "#a1efe4"
1548 "#a6e22e"
1549 "#e6db74"
1550 "#fd971f"
1551 "#f92672"
1552 "#fd5ff0"
1553 "#ae81ff"))
1554
1555 (context-coloring-define-theme
1556 'solarized
1557 :recede t
1558 :aliases '(solarized-light
1559 solarized-dark
1560 sanityinc-solarized-light
1561 sanityinc-solarized-dark)
1562 :colors '("#839496"
1563 "#268bd2"
1564 "#2aa198"
1565 "#859900"
1566 "#b58900"
1567 "#cb4b16"
1568 "#dc322f"
1569 "#d33682"
1570 "#6c71c4"
1571 "#69b7f0"
1572 "#69cabf"
1573 "#b4c342"
1574 "#deb542"
1575 "#f2804f"
1576 "#ff6e64"
1577 "#f771ac"
1578 "#9ea0e5"))
1579
1580 (context-coloring-define-theme
1581 'spacegray
1582 :recede t
1583 :colors '("#ffffff"
1584 "#89aaeb"
1585 "#c189eb"
1586 "#bf616a"
1587 "#dca432"
1588 "#ebcb8b"
1589 "#b4eb89"
1590 "#89ebca"))
1591
1592 (context-coloring-define-theme
1593 'tango
1594 :recede t
1595 :colors '("#2e3436"
1596 "#346604"
1597 "#204a87"
1598 "#5c3566"
1599 "#a40000"
1600 "#b35000"
1601 "#c4a000"
1602 "#8ae234"
1603 "#8cc4ff"
1604 "#ad7fa8"
1605 "#ef2929"
1606 "#fcaf3e"
1607 "#fce94f"))
1608
1609 (context-coloring-define-theme
1610 'zenburn
1611 :recede t
1612 :colors '("#dcdccc"
1613 "#93e0e3"
1614 "#bfebbf"
1615 "#f0dfaf"
1616 "#dfaf8f"
1617 "#cc9393"
1618 "#dc8cc3"
1619 "#94bff3"
1620 "#9fc59f"
1621 "#d0bf8f"
1622 "#dca3a3"))
1623
1624
1625 ;;; Change detection
1626
1627 (defvar-local context-coloring-colorize-idle-timer nil
1628 "The currently-running idle timer.")
1629
1630 (defcustom context-coloring-delay 0.25
1631 "Delay between a buffer update and colorization.
1632
1633 Increase this if your machine is high-performing. Decrease it if
1634 it ain't.
1635
1636 Supported modes: `js-mode', `js3-mode', `emacs-lisp-mode'"
1637 :group 'context-coloring)
1638
1639 (defun context-coloring-setup-idle-change-detection ()
1640 "Setup idle change detection."
1641 (add-hook
1642 'after-change-functions 'context-coloring-change-function nil t)
1643 (add-hook
1644 'kill-buffer-hook 'context-coloring-teardown-idle-change-detection nil t)
1645 (setq context-coloring-colorize-idle-timer
1646 (run-with-idle-timer
1647 context-coloring-delay
1648 t
1649 'context-coloring-maybe-colorize
1650 (current-buffer))))
1651
1652 (defun context-coloring-teardown-idle-change-detection ()
1653 "Teardown idle change detection."
1654 (context-coloring-kill-scopifier)
1655 (when context-coloring-colorize-idle-timer
1656 (cancel-timer context-coloring-colorize-idle-timer))
1657 (remove-hook
1658 'kill-buffer-hook 'context-coloring-teardown-idle-change-detection t)
1659 (remove-hook
1660 'after-change-functions 'context-coloring-change-function t))
1661
1662
1663 ;;; Built-in dispatches
1664
1665 (context-coloring-define-dispatch
1666 'javascript-node
1667 :modes '(js-mode js3-mode)
1668 :executable "scopifier"
1669 :command "scopifier"
1670 :version "v1.1.1")
1671
1672 (context-coloring-define-dispatch
1673 'javascript-js2
1674 :modes '(js2-mode)
1675 :colorizer 'context-coloring-js2-colorize
1676 :setup
1677 (lambda ()
1678 (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t))
1679 :teardown
1680 (lambda ()
1681 (remove-hook 'js2-post-parse-callbacks 'context-coloring-colorize t)))
1682
1683 (context-coloring-define-dispatch
1684 'emacs-lisp
1685 :modes '(emacs-lisp-mode)
1686 :colorizer 'context-coloring-elisp-colorize-buffer
1687 :setup 'context-coloring-setup-idle-change-detection
1688 :teardown 'context-coloring-teardown-idle-change-detection)
1689
1690 (defun context-coloring-dispatch (&optional callback)
1691 "Determine the optimal track for scopification / coloring of
1692 the current buffer, then execute it.
1693
1694 Invoke CALLBACK when complete. It is invoked synchronously for
1695 elisp tracks, and asynchronously for shell command tracks."
1696 (let* ((dispatch (context-coloring-get-dispatch-for-mode major-mode))
1697 (colorizer (plist-get dispatch :colorizer))
1698 (scopifier (plist-get dispatch :scopifier))
1699 (command (plist-get dispatch :command))
1700 interrupted-p)
1701 (cond
1702 ((or colorizer scopifier)
1703 (setq interrupted-p
1704 (catch 'interrupted
1705 (cond
1706 (colorizer
1707 (funcall colorizer))
1708 (scopifier
1709 (context-coloring-apply-tokens (funcall scopifier))))))
1710 (cond
1711 (interrupted-p
1712 (setq context-coloring-changed t))
1713 (t
1714 (when callback (funcall callback)))))
1715 (command
1716 (context-coloring-scopify-and-colorize command callback)))))
1717
1718
1719 ;;; Minor mode
1720
1721 ;;;###autoload
1722 (define-minor-mode context-coloring-mode
1723 "Context-based code coloring, inspired by Douglas Crockford."
1724 nil " Context" nil
1725 (if (not context-coloring-mode)
1726 (progn
1727 (let ((dispatch (context-coloring-get-dispatch-for-mode major-mode)))
1728 (when dispatch
1729 (let ((command (plist-get dispatch :command))
1730 (teardown (plist-get dispatch :teardown)))
1731 (when command
1732 (context-coloring-teardown-idle-change-detection))
1733 (when teardown
1734 (funcall teardown)))))
1735 (font-lock-mode)
1736 (jit-lock-mode t))
1737
1738 ;; Font lock is incompatible with this mode; the converse is also true.
1739 (font-lock-mode 0)
1740 (jit-lock-mode nil)
1741
1742 ;; ...but we do use font-lock functions here.
1743 (font-lock-set-defaults)
1744
1745 ;; Safely change the valye of this function as necessary.
1746 (make-local-variable 'font-lock-syntactic-face-function)
1747
1748 (let ((dispatch (context-coloring-get-dispatch-for-mode major-mode)))
1749 (if dispatch
1750 (progn
1751 (let ((command (plist-get dispatch :command))
1752 (version (plist-get dispatch :version))
1753 (executable (plist-get dispatch :executable))
1754 (setup (plist-get dispatch :setup))
1755 (colorize-initially-p t))
1756 (when command
1757 ;; Shell commands recolor on change, idly.
1758 (cond
1759 ((and executable
1760 (null (executable-find executable)))
1761 (message "Executable \"%s\" not found" executable)
1762 (setq colorize-initially-p nil))
1763 (version
1764 (context-coloring-check-scopifier-version
1765 (lambda (sufficient-p)
1766 (if sufficient-p
1767 (progn
1768 (context-coloring-setup-idle-change-detection)
1769 (context-coloring-colorize))
1770 (message "Update to the minimum version of \"%s\" (%s)"
1771 executable version))))
1772 (setq colorize-initially-p nil))
1773 (t
1774 (context-coloring-setup-idle-change-detection))))
1775 (when setup
1776 (funcall setup))
1777 ;; Colorize once initially.
1778 (when colorize-initially-p
1779 (let ((context-coloring-parse-interruptable-p nil))
1780 (context-coloring-colorize)))))
1781 (when (null dispatch)
1782 (message "Context coloring is not available for this major mode"))))))
1783
1784 (provide 'context-coloring)
1785
1786 ;;; context-coloring.el ends here