]> code.delx.au - gnu-emacs-elpa/blob - context-coloring.el
Remove asynchronous support.
[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.5.0
7 ;; Keywords: convenience faces tools
8 ;; Package-Requires: ((emacs "24.3") (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
47 ;;; Faces
48
49 (defun context-coloring-defface (level tty light dark)
50 "Define a face for LEVEL with colors for TTY, LIGHT and DARK
51 backgrounds."
52 (let ((face (intern (format "context-coloring-level-%s-face" level)))
53 (doc (format "Context coloring face, level %s." level)))
54 (custom-declare-face
55 face
56 `((((type tty)) (:foreground ,tty))
57 (((background light)) (:foreground ,light))
58 (((background dark)) (:foreground ,dark)))
59 doc
60 :group 'context-coloring)))
61
62 (defun context-coloring-defface-neutral (level)
63 "Define a face for LEVEL with the default neutral colors."
64 (context-coloring-defface level nil "#3f3f3f" "#cdcdcd"))
65
66 (context-coloring-defface 0 nil "#000000" "#ffffff")
67 (context-coloring-defface 1 "yellow" "#008b8b" "#00ffff")
68 (context-coloring-defface 2 "green" "#0000ff" "#87cefa")
69 (context-coloring-defface 3 "cyan" "#483d8b" "#b0c4de")
70 (context-coloring-defface 4 "blue" "#a020f0" "#eedd82")
71 (context-coloring-defface 5 "magenta" "#a0522d" "#98fb98")
72 (context-coloring-defface 6 "red" "#228b22" "#7fffd4")
73 (context-coloring-defface-neutral 7)
74
75 (defvar context-coloring-maximum-face nil
76 "Index of the highest face available for coloring.")
77
78 (defvar context-coloring-original-maximum-face nil
79 "Fallback value for `context-coloring-maximum-face' when all
80 themes have been disabled.")
81
82 (setq context-coloring-maximum-face 7)
83
84 (setq context-coloring-original-maximum-face
85 context-coloring-maximum-face)
86
87 ;; Theme authors can have up to 26 levels: 1 (0th) for globals, 24 (1st-24th)
88 ;; for nested levels, and 1 (25th) for infinity.
89 (dotimes (number 18)
90 (context-coloring-defface-neutral (+ number context-coloring-maximum-face 1)))
91
92
93 ;;; Face functions
94
95 (defsubst context-coloring-level-face (level)
96 "Return the symbol for a face with LEVEL."
97 ;; `concat' is faster than `format' here.
98 (intern-soft
99 (concat "context-coloring-level-" (number-to-string level) "-face")))
100
101 (defsubst context-coloring-bounded-level-face (level)
102 "Return the symbol for a face with LEVEL, bounded by
103 `context-coloring-maximum-face'."
104 (context-coloring-level-face (min level context-coloring-maximum-face)))
105
106
107 ;;; Change detection
108
109 (defvar-local context-coloring-changed-p nil
110 "Indication that the buffer has changed recently, which implies
111 that it should be colored again by
112 `context-coloring-maybe-colorize-idle-timer' if that timer is
113 being used.")
114
115 (defvar-local context-coloring-changed-start nil
116 "Beginning of last text that changed.")
117
118 (defvar-local context-coloring-changed-end nil
119 "End of last text that changed.")
120
121 (defvar-local context-coloring-changed-length nil
122 "Length of last text that changed.")
123
124 (defun context-coloring-change-function (start end length)
125 "Register a change so that a buffer can be colorized soon.
126
127 START, END and LENGTH are recorded for later use."
128 ;; Tokenization is obsolete if there was a change.
129 (setq context-coloring-changed-start start)
130 (setq context-coloring-changed-end end)
131 (setq context-coloring-changed-length length)
132 (setq context-coloring-changed-p t))
133
134 (defun context-coloring-maybe-colorize-with-buffer (buffer)
135 "Color BUFFER and if it has changed."
136 (when (and (eq buffer (current-buffer))
137 context-coloring-changed-p)
138 (context-coloring-colorize-with-buffer buffer)
139 (setq context-coloring-changed-p nil)
140 (setq context-coloring-changed-start nil)
141 (setq context-coloring-changed-end nil)
142 (setq context-coloring-changed-length nil)))
143
144 (defvar-local context-coloring-maybe-colorize-idle-timer nil
145 "The currently-running idle timer for conditional coloring.")
146
147 (defvar-local context-coloring-colorize-idle-timer nil
148 "The currently-running idle timer for unconditional coloring.")
149
150 (defcustom context-coloring-default-delay 0.25
151 "Default delay between a buffer update and colorization.
152
153 Increase this if your machine is high-performing. Decrease it if
154 it ain't."
155 :group 'context-coloring)
156
157 (make-obsolete-variable
158 'context-coloring-delay
159 'context-coloring-default-delay
160 "6.4.0")
161
162 (defun context-coloring-cancel-timer (timer)
163 "Cancel TIMER."
164 (when timer
165 (cancel-timer timer)))
166
167 (defun context-coloring-schedule-coloring (time)
168 "Schedule coloring to occur once after Emacs is idle for TIME."
169 (context-coloring-cancel-timer context-coloring-colorize-idle-timer)
170 (setq context-coloring-colorize-idle-timer
171 (run-with-idle-timer
172 time
173 nil
174 #'context-coloring-colorize-with-buffer
175 (current-buffer))))
176
177 (defun context-coloring-setup-idle-change-detection ()
178 "Setup idle change detection."
179 (let ((dispatch (context-coloring-get-current-dispatch)))
180 (add-hook
181 'after-change-functions #'context-coloring-change-function nil t)
182 (add-hook
183 'kill-buffer-hook #'context-coloring-teardown-idle-change-detection nil t)
184 (setq context-coloring-maybe-colorize-idle-timer
185 (run-with-idle-timer
186 (or (plist-get dispatch :delay) context-coloring-default-delay)
187 t
188 #'context-coloring-maybe-colorize-with-buffer
189 (current-buffer)))))
190
191 (defun context-coloring-teardown-idle-change-detection ()
192 "Teardown idle change detection."
193 (dolist (timer (list context-coloring-colorize-idle-timer
194 context-coloring-maybe-colorize-idle-timer))
195 (context-coloring-cancel-timer timer))
196 (remove-hook
197 'kill-buffer-hook #'context-coloring-teardown-idle-change-detection t)
198 (remove-hook
199 'after-change-functions #'context-coloring-change-function t))
200
201
202 ;;; Colorization utilities
203
204 (defsubst context-coloring-colorize-region (start end level)
205 "Color characters from the 1-indexed START point (inclusive) to
206 the END point (exclusive) with the face corresponding to LEVEL."
207 (add-text-properties
208 start
209 end
210 `(face ,(context-coloring-bounded-level-face level))))
211
212 (make-obsolete-variable
213 'context-coloring-comments-and-strings
214 "use `context-coloring-syntactic-comments' and
215 `context-coloring-syntactic-strings' instead."
216 "6.1.0")
217
218 (defcustom context-coloring-syntactic-comments t
219 "If non-nil, also color comments using `font-lock'."
220 :group 'context-coloring)
221
222 (defcustom context-coloring-syntactic-strings t
223 "If non-nil, also color strings using `font-lock'."
224 :group 'context-coloring)
225
226 (defun context-coloring-font-lock-syntactic-comment-function (state)
227 "Tell `font-lock' to color a comment but not a string according
228 to STATE."
229 (if (nth 3 state) nil font-lock-comment-face))
230
231 (defun context-coloring-font-lock-syntactic-string-function (state)
232 "Tell `font-lock' to color a string but not a comment according
233 to STATE."
234 (if (nth 3 state) font-lock-string-face nil))
235
236 (defsubst context-coloring-colorize-comments-and-strings (&optional min max)
237 "Color the current buffer's comments or strings if
238 `context-coloring-syntactic-comments' or
239 `context-coloring-syntactic-strings' are non-nil. MIN defaults
240 to the beginning of the buffer and MAX defaults to the end."
241 (when (or context-coloring-syntactic-comments
242 context-coloring-syntactic-strings)
243 (let ((min (or min (point-min)))
244 (max (or max (point-max)))
245 (font-lock-syntactic-face-function
246 (cond
247 ((and context-coloring-syntactic-comments
248 (not context-coloring-syntactic-strings))
249 #'context-coloring-font-lock-syntactic-comment-function)
250 ((and context-coloring-syntactic-strings
251 (not context-coloring-syntactic-comments))
252 #'context-coloring-font-lock-syntactic-string-function)
253 (t
254 font-lock-syntactic-face-function))))
255 (save-excursion
256 (font-lock-fontify-syntactically-region min max)
257 ;; TODO: Make configurable at the dispatch level.
258 (when (eq major-mode 'emacs-lisp-mode)
259 (font-lock-fontify-keywords-region min max))))))
260
261
262 ;;; js2-mode colorization
263
264 (defvar-local context-coloring-js2-scope-level-hash-table nil
265 "Associate `js2-scope' structures and with their scope
266 levels.")
267
268 (defcustom context-coloring-javascript-block-scopes nil
269 "If non-nil, also color block scopes in the scope hierarchy in JavaScript.
270
271 The block-scoped `let' and `const' are introduced in ES6. Enable
272 this for ES6 code; disable it elsewhere."
273 :group 'context-coloring)
274
275 (make-obsolete-variable
276 'context-coloring-js-block-scopes
277 'context-coloring-javascript-block-scopes
278 "7.0.0")
279
280 (defsubst context-coloring-js2-scope-level (scope)
281 "Return the level of SCOPE."
282 (cond ((gethash scope context-coloring-js2-scope-level-hash-table))
283 (t
284 (let ((level 0)
285 (current-scope scope)
286 enclosing-scope)
287 (while (and current-scope
288 (js2-node-parent current-scope)
289 (setq enclosing-scope
290 (js2-node-get-enclosing-scope current-scope)))
291 (when (or context-coloring-javascript-block-scopes
292 (let ((type (js2-scope-type current-scope)))
293 (or (= type js2-SCRIPT)
294 (= type js2-FUNCTION)
295 (= type js2-CATCH))))
296 (setq level (+ level 1)))
297 (setq current-scope enclosing-scope))
298 (puthash scope level context-coloring-js2-scope-level-hash-table)))))
299
300 (defsubst context-coloring-js2-local-name-node-p (node)
301 "Determine if NODE is a `js2-name-node' representing a local
302 variable."
303 (and (js2-name-node-p node)
304 (let ((parent (js2-node-parent node)))
305 (not (or (and (js2-object-prop-node-p parent)
306 (eq node (js2-object-prop-node-left parent)))
307 (and (js2-prop-get-node-p parent)
308 ;; For nested property lookup, the node on the left is a
309 ;; `js2-prop-get-node', so this always works.
310 (eq node (js2-prop-get-node-right parent))))))))
311
312 (defvar-local context-coloring-point-max nil
313 "Cached value of `point-max'.")
314
315 (defsubst context-coloring-js2-colorize-node (node level)
316 "Color NODE with the color for LEVEL."
317 (let ((start (js2-node-abs-pos node)))
318 (context-coloring-colorize-region
319 start
320 (min
321 ;; End
322 (+ start (js2-node-len node))
323 ;; Somes nodes (like the ast when there is an unterminated multiline
324 ;; comment) will stretch to the value of `point-max'.
325 context-coloring-point-max)
326 level)))
327
328 (defun context-coloring-js2-colorize ()
329 "Color the current buffer using the abstract syntax tree
330 generated by `js2-mode'."
331 ;; Reset the hash table; the old one could be obsolete.
332 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test #'eq))
333 (setq context-coloring-point-max (point-max))
334 (with-silent-modifications
335 (js2-visit-ast
336 js2-mode-ast
337 (lambda (node end-p)
338 (when (null end-p)
339 (cond
340 ((js2-scope-p node)
341 (context-coloring-js2-colorize-node
342 node
343 (context-coloring-js2-scope-level node)))
344 ((context-coloring-js2-local-name-node-p node)
345 (let* ((enclosing-scope (js2-node-get-enclosing-scope node))
346 (defining-scope (js2-get-defining-scope
347 enclosing-scope
348 (js2-name-node-name node))))
349 ;; The tree seems to be walked lexically, so an entire scope will
350 ;; be colored, including its name nodes, before they are reached.
351 ;; Coloring the nodes defined in that scope would be redundant, so
352 ;; don't do it.
353 (when (not (eq defining-scope enclosing-scope))
354 (context-coloring-js2-colorize-node
355 node
356 (context-coloring-js2-scope-level defining-scope))))))
357 ;; The `t' indicates to search children.
358 t)))
359 (context-coloring-colorize-comments-and-strings)))
360
361
362 ;;; Emacs Lisp colorization
363
364 (defsubst context-coloring-forward-sws ()
365 "Move forward through whitespace and comments."
366 (while (forward-comment 1)))
367
368 (defsubst context-coloring-elisp-forward-sws ()
369 "Move forward through whitespace and comments, colorizing
370 comments along the way."
371 (let ((start (point)))
372 (context-coloring-forward-sws)
373 (context-coloring-colorize-comments-and-strings start (point))))
374
375 (defsubst context-coloring-elisp-forward-sexp ()
376 "Like `forward-sexp', but colorize comments and strings along
377 the way."
378 (let ((start (point)))
379 (forward-sexp)
380 (context-coloring-elisp-colorize-comments-and-strings-in-region
381 start (point))))
382
383 (defsubst context-coloring-get-syntax-code ()
384 "Get the syntax code at point."
385 (syntax-class
386 ;; Faster version of `syntax-after':
387 (aref (syntax-table) (char-after (point)))))
388
389 (defsubst context-coloring-exact-regexp (word)
390 "Create a regexp matching exactly WORD."
391 (concat "\\`" (regexp-quote word) "\\'"))
392
393 (defsubst context-coloring-exact-or-regexp (words)
394 "Create a regexp matching any exact word in WORDS."
395 (context-coloring-join
396 (mapcar #'context-coloring-exact-regexp words) "\\|"))
397
398 (defconst context-coloring-elisp-ignored-word-regexp
399 (context-coloring-join (list "\\`[-+]?[0-9]"
400 "\\`[&:].+"
401 (context-coloring-exact-or-regexp
402 '("t" "nil" "." "?")))
403 "\\|")
404 "Match words that might be considered symbols but can't be
405 bound as variables.")
406
407 (defconst context-coloring-WORD-CODE 2)
408 (defconst context-coloring-SYMBOL-CODE 3)
409 (defconst context-coloring-OPEN-PARENTHESIS-CODE 4)
410 (defconst context-coloring-CLOSE-PARENTHESIS-CODE 5)
411 (defconst context-coloring-EXPRESSION-PREFIX-CODE 6)
412 (defconst context-coloring-STRING-QUOTE-CODE 7)
413 (defconst context-coloring-ESCAPE-CODE 9)
414 (defconst context-coloring-COMMENT-START-CODE 11)
415 (defconst context-coloring-COMMENT-END-CODE 12)
416
417 (defconst context-coloring-OCTOTHORPE-CHAR (string-to-char "#"))
418 (defconst context-coloring-APOSTROPHE-CHAR (string-to-char "'"))
419 (defconst context-coloring-OPEN-PARENTHESIS-CHAR (string-to-char "("))
420 (defconst context-coloring-COMMA-CHAR (string-to-char ","))
421 (defconst context-coloring-AT-CHAR (string-to-char "@"))
422 (defconst context-coloring-BACKTICK-CHAR (string-to-char "`"))
423
424 (defsubst context-coloring-elisp-identifier-p (syntax-code)
425 "Check if SYNTAX-CODE is an elisp identifier constituent."
426 (or (= syntax-code context-coloring-WORD-CODE)
427 (= syntax-code context-coloring-SYMBOL-CODE)))
428
429 (defvar context-coloring-parse-interruptable-p t
430 "Set this to nil to force parse to continue until finished.")
431
432 (defconst context-coloring-elisp-sexps-per-pause 350
433 "Pause after this many iterations to check for user input.
434 If user input is pending, stop the parse. This makes for a
435 smoother user experience for large files.
436
437 This number should trigger pausing at about 60 frames per
438 second.")
439
440 (defvar context-coloring-elisp-sexp-count 0
441 "Current number of sexps leading up to the next pause.")
442
443 (defsubst context-coloring-elisp-increment-sexp-count ()
444 "Maybe check if the current parse should be interrupted as a
445 result of pending user input."
446 (setq context-coloring-elisp-sexp-count
447 (1+ context-coloring-elisp-sexp-count))
448 (when (and (zerop (% context-coloring-elisp-sexp-count
449 context-coloring-elisp-sexps-per-pause))
450 context-coloring-parse-interruptable-p
451 (input-pending-p))
452 (throw 'interrupted t)))
453
454 (defvar context-coloring-elisp-scope-stack '()
455 "List of scopes in the current parse.")
456
457 (defsubst context-coloring-elisp-make-scope (level)
458 "Make a scope object for LEVEL."
459 (list
460 :level level
461 :variables '()))
462
463 (defsubst context-coloring-elisp-scope-get-level (scope)
464 "Get the level of SCOPE object."
465 (plist-get scope :level))
466
467 (defsubst context-coloring-elisp-scope-add-variable (scope variable)
468 "Add to SCOPE a VARIABLE."
469 (plist-put scope :variables (cons variable (plist-get scope :variables))))
470
471 (defsubst context-coloring-elisp-scope-has-variable (scope variable)
472 "Check if SCOPE has VARIABLE."
473 (member variable (plist-get scope :variables)))
474
475 (defsubst context-coloring-elisp-get-variable-level (variable)
476 "Search up the scope chain for the first instance of VARIABLE
477 and return its level, or 0 (global) if it isn't found."
478 (let* ((scope-stack context-coloring-elisp-scope-stack)
479 scope
480 level)
481 (while (and scope-stack (not level))
482 (setq scope (car scope-stack))
483 (cond
484 ((context-coloring-elisp-scope-has-variable scope variable)
485 (setq level (context-coloring-elisp-scope-get-level scope)))
486 (t
487 (setq scope-stack (cdr scope-stack)))))
488 ;; Assume a global variable.
489 (or level 0)))
490
491 (defsubst context-coloring-elisp-get-current-scope-level ()
492 "Get the nesting level of the current scope."
493 (cond
494 ((car context-coloring-elisp-scope-stack)
495 (context-coloring-elisp-scope-get-level (car context-coloring-elisp-scope-stack)))
496 (t
497 0)))
498
499 (defsubst context-coloring-elisp-push-scope ()
500 "Add a new scope to the bottom of the scope chain."
501 (push (context-coloring-elisp-make-scope
502 (1+ (context-coloring-elisp-get-current-scope-level)))
503 context-coloring-elisp-scope-stack))
504
505 (defsubst context-coloring-elisp-pop-scope ()
506 "Remove the scope on the bottom of the scope chain."
507 (pop context-coloring-elisp-scope-stack))
508
509 (defsubst context-coloring-elisp-add-variable (variable)
510 "Add VARIABLE to the current scope."
511 (context-coloring-elisp-scope-add-variable
512 (car context-coloring-elisp-scope-stack)
513 variable))
514
515 (defsubst context-coloring-elisp-parse-bindable (callback)
516 "Parse the symbol at point, and if the symbol can be bound,
517 invoke CALLBACK with it."
518 (let* ((arg-string (buffer-substring-no-properties
519 (point)
520 (progn (context-coloring-elisp-forward-sexp)
521 (point)))))
522 (when (not (string-match-p
523 context-coloring-elisp-ignored-word-regexp
524 arg-string))
525 (funcall callback arg-string))))
526
527 (defun context-coloring-elisp-parse-let-varlist (type)
528 "Parse the list of variable initializers at point. If TYPE is
529 `let', all the variables are bound after all their initializers
530 are parsed; if TYPE is `let*', each variable is bound immediately
531 after its own initializer is parsed."
532 (let ((varlist '())
533 syntax-code)
534 ;; Enter.
535 (forward-char)
536 (while (/= (setq syntax-code (context-coloring-get-syntax-code))
537 context-coloring-CLOSE-PARENTHESIS-CODE)
538 (cond
539 ((= syntax-code context-coloring-OPEN-PARENTHESIS-CODE)
540 (forward-char)
541 (context-coloring-elisp-forward-sws)
542 (setq syntax-code (context-coloring-get-syntax-code))
543 (when (context-coloring-elisp-identifier-p syntax-code)
544 (context-coloring-elisp-parse-bindable
545 (lambda (var)
546 (push var varlist)))
547 (context-coloring-elisp-forward-sws)
548 (setq syntax-code (context-coloring-get-syntax-code))
549 (when (/= syntax-code context-coloring-CLOSE-PARENTHESIS-CODE)
550 (context-coloring-elisp-colorize-sexp)))
551 (context-coloring-elisp-forward-sws)
552 ;; Skip past the closing parenthesis.
553 (forward-char))
554 ((context-coloring-elisp-identifier-p syntax-code)
555 (context-coloring-elisp-parse-bindable
556 (lambda (var)
557 (push var varlist))))
558 (t
559 ;; Ignore artifacts.
560 (context-coloring-elisp-forward-sexp)))
561 (when (eq type 'let*)
562 (context-coloring-elisp-add-variable (pop varlist)))
563 (context-coloring-elisp-forward-sws))
564 (when (eq type 'let)
565 (while varlist
566 (context-coloring-elisp-add-variable (pop varlist))))
567 ;; Exit.
568 (forward-char)))
569
570 (defun context-coloring-elisp-parse-arglist ()
571 "Parse the list of function arguments at point."
572 (let (syntax-code)
573 ;; Enter.
574 (forward-char)
575 (while (/= (setq syntax-code (context-coloring-get-syntax-code))
576 context-coloring-CLOSE-PARENTHESIS-CODE)
577 (cond
578 ((context-coloring-elisp-identifier-p syntax-code)
579 (context-coloring-elisp-parse-bindable
580 (lambda (arg)
581 (context-coloring-elisp-add-variable arg))))
582 (t
583 ;; Ignore artifacts.
584 (context-coloring-elisp-forward-sexp)))
585 (context-coloring-elisp-forward-sws))
586 ;; Exit.
587 (forward-char)))
588
589 (defun context-coloring-elisp-skip-callee-name ()
590 "Skip past the opening parenthesis and name of a function."
591 ;; Enter.
592 (forward-char)
593 (context-coloring-elisp-forward-sws)
594 ;; Skip past the function name.
595 (forward-sexp)
596 (context-coloring-elisp-forward-sws))
597
598 (defun context-coloring-elisp-colorize-scope (callback)
599 "Color the whole scope at point with its one color. Handle a
600 header in CALLBACK."
601 (let ((start (point))
602 (end (progn (forward-sexp)
603 (point))))
604 (context-coloring-elisp-push-scope)
605 ;; Splash the whole thing in one color.
606 (context-coloring-colorize-region
607 start
608 end
609 (context-coloring-elisp-get-current-scope-level))
610 ;; Even if the parse is interrupted, this region should still be colored
611 ;; syntactically.
612 (context-coloring-elisp-colorize-comments-and-strings-in-region
613 start
614 end)
615 (goto-char start)
616 (context-coloring-elisp-skip-callee-name)
617 (funcall callback)
618 (context-coloring-elisp-colorize-region (point) (1- end))
619 ;; Exit.
620 (forward-char)
621 (context-coloring-elisp-pop-scope)))
622
623 (defun context-coloring-elisp-parse-header (callback)
624 "Parse a function header at point with CALLBACK."
625 (when (= (context-coloring-get-syntax-code) context-coloring-OPEN-PARENTHESIS-CODE)
626 (funcall callback)))
627
628 (defun context-coloring-elisp-colorize-defun-like (callback)
629 "Color the defun-like function at point, parsing the header
630 with CALLBACK."
631 (context-coloring-elisp-colorize-scope
632 (lambda ()
633 (when (context-coloring-elisp-identifier-p (context-coloring-get-syntax-code))
634 ;; Color the defun's name with the top-level color.
635 (context-coloring-colorize-region
636 (point)
637 (progn (forward-sexp)
638 (point))
639 0)
640 (context-coloring-elisp-forward-sws)
641 (context-coloring-elisp-parse-header callback)))))
642
643 (defun context-coloring-elisp-colorize-defun ()
644 "Color the `defun' at point."
645 (context-coloring-elisp-colorize-defun-like
646 'context-coloring-elisp-parse-arglist))
647
648 (defun context-coloring-elisp-colorize-defadvice ()
649 "Color the `defadvice' at point."
650 (context-coloring-elisp-colorize-defun-like
651 (lambda ()
652 (let (syntax-code)
653 ;; Enter.
654 (forward-char)
655 (while (/= (setq syntax-code (context-coloring-get-syntax-code))
656 context-coloring-CLOSE-PARENTHESIS-CODE)
657 (cond
658 ((= syntax-code context-coloring-OPEN-PARENTHESIS-CODE)
659 (context-coloring-elisp-parse-arglist))
660 (t
661 ;; Ignore artifacts.
662 (context-coloring-elisp-forward-sexp)))
663 (context-coloring-elisp-forward-sws))))))
664
665 (defun context-coloring-elisp-colorize-lambda-like (callback)
666 "Color the lambda-like function at point, parsing the header
667 with CALLBACK."
668 (context-coloring-elisp-colorize-scope
669 (lambda ()
670 (context-coloring-elisp-parse-header callback))))
671
672 (defun context-coloring-elisp-colorize-lambda ()
673 "Color the `lambda' at point."
674 (context-coloring-elisp-colorize-lambda-like
675 'context-coloring-elisp-parse-arglist))
676
677 (defun context-coloring-elisp-colorize-let ()
678 "Color the `let' at point."
679 (context-coloring-elisp-colorize-lambda-like
680 (lambda ()
681 (context-coloring-elisp-parse-let-varlist 'let))))
682
683 (defun context-coloring-elisp-colorize-let* ()
684 "Color the `let*' at point."
685 (context-coloring-elisp-colorize-lambda-like
686 (lambda ()
687 (context-coloring-elisp-parse-let-varlist 'let*))))
688
689 (defun context-coloring-elisp-colorize-cond ()
690 "Color the `cond' at point."
691 (let (syntax-code)
692 (context-coloring-elisp-skip-callee-name)
693 (while (/= (setq syntax-code (context-coloring-get-syntax-code))
694 context-coloring-CLOSE-PARENTHESIS-CODE)
695 (cond
696 ((= syntax-code context-coloring-OPEN-PARENTHESIS-CODE)
697 ;; Colorize inside the parens.
698 (let ((start (point)))
699 (forward-sexp)
700 (context-coloring-elisp-colorize-region
701 (1+ start) (1- (point)))
702 ;; Exit.
703 (forward-char)))
704 (t
705 ;; Ignore artifacts.
706 (context-coloring-elisp-forward-sexp)))
707 (context-coloring-elisp-forward-sws))
708 ;; Exit.
709 (forward-char)))
710
711 (defun context-coloring-elisp-colorize-condition-case ()
712 "Color the `condition-case' at point."
713 (let (syntax-code
714 variable
715 case-pos
716 case-end)
717 (context-coloring-elisp-colorize-scope
718 (lambda ()
719 (setq syntax-code (context-coloring-get-syntax-code))
720 ;; Gracefully ignore missing variables.
721 (when (context-coloring-elisp-identifier-p syntax-code)
722 (context-coloring-elisp-parse-bindable
723 (lambda (parsed-variable)
724 (setq variable parsed-variable)))
725 (context-coloring-elisp-forward-sws))
726 (context-coloring-elisp-colorize-sexp)
727 (context-coloring-elisp-forward-sws)
728 ;; Parse the handlers with the error variable in scope.
729 (when variable
730 (context-coloring-elisp-add-variable variable))
731 (while (/= (setq syntax-code (context-coloring-get-syntax-code))
732 context-coloring-CLOSE-PARENTHESIS-CODE)
733 (cond
734 ((= syntax-code context-coloring-OPEN-PARENTHESIS-CODE)
735 (setq case-pos (point))
736 (context-coloring-elisp-forward-sexp)
737 (setq case-end (point))
738 (goto-char case-pos)
739 ;; Enter.
740 (forward-char)
741 (context-coloring-elisp-forward-sws)
742 (setq syntax-code (context-coloring-get-syntax-code))
743 (when (/= syntax-code context-coloring-CLOSE-PARENTHESIS-CODE)
744 ;; Skip the condition name(s).
745 (context-coloring-elisp-forward-sexp)
746 ;; Color the remaining portion of the handler.
747 (context-coloring-elisp-colorize-region
748 (point)
749 (1- case-end)))
750 ;; Exit.
751 (forward-char))
752 (t
753 ;; Ignore artifacts.
754 (context-coloring-elisp-forward-sexp)))
755 (context-coloring-elisp-forward-sws))))))
756
757 (defun context-coloring-elisp-colorize-dolist ()
758 "Color the `dolist' at point."
759 (let (syntax-code
760 (index 0))
761 (context-coloring-elisp-colorize-scope
762 (lambda ()
763 (setq syntax-code (context-coloring-get-syntax-code))
764 (when (= syntax-code context-coloring-OPEN-PARENTHESIS-CODE)
765 (forward-char)
766 (context-coloring-elisp-forward-sws)
767 (while (/= (setq syntax-code (context-coloring-get-syntax-code))
768 context-coloring-CLOSE-PARENTHESIS-CODE)
769 (cond
770 ((and
771 (or (= index 0) (= index 2))
772 (context-coloring-elisp-identifier-p syntax-code))
773 ;; Add the first or third name to the scope.
774 (context-coloring-elisp-parse-bindable
775 (lambda (variable)
776 (context-coloring-elisp-add-variable variable))))
777 (t
778 ;; Color artifacts.
779 (context-coloring-elisp-colorize-sexp)))
780 (context-coloring-elisp-forward-sws)
781 (setq index (1+ index)))
782 ;; Exit.
783 (forward-char))))))
784
785 (defun context-coloring-elisp-colorize-quote ()
786 "Color the `quote' at point."
787 (let* ((start (point))
788 (end (progn (forward-sexp)
789 (point))))
790 (context-coloring-colorize-region
791 start
792 end
793 (context-coloring-elisp-get-current-scope-level))
794 (context-coloring-elisp-colorize-comments-and-strings-in-region start end)))
795
796 (defvar context-coloring-elisp-callee-dispatch-hash-table
797 (let ((table (make-hash-table :test 'equal)))
798 (dolist (callee '("defun" "defun*" "defsubst" "defmacro" "cl-defun" "cl-defsubst" "cl-defmacro"))
799 (puthash callee #'context-coloring-elisp-colorize-defun table))
800 (dolist (callee '("condition-case" "condition-case-unless-debug"))
801 (puthash callee #'context-coloring-elisp-colorize-condition-case table))
802 (dolist (callee '("dolist" "dotimes"))
803 (puthash callee #'context-coloring-elisp-colorize-dolist table))
804 (puthash "let" #'context-coloring-elisp-colorize-let table)
805 (puthash "let*" #'context-coloring-elisp-colorize-let* table)
806 (puthash "lambda" #'context-coloring-elisp-colorize-lambda table)
807 (puthash "cond" #'context-coloring-elisp-colorize-cond table)
808 (puthash "defadvice" #'context-coloring-elisp-colorize-defadvice table)
809 (puthash "quote" #'context-coloring-elisp-colorize-quote table)
810 (puthash "backquote" #'context-coloring-elisp-colorize-backquote table)
811 table)
812 "Map function names to their coloring functions.")
813
814 (defun context-coloring-elisp-colorize-parenthesized-sexp ()
815 "Color the sexp enclosed by parenthesis at point."
816 (context-coloring-elisp-increment-sexp-count)
817 (let* ((start (point))
818 (end (progn (forward-sexp)
819 (point)))
820 (syntax-code (progn (goto-char start)
821 (forward-char)
822 ;; Coloring is unnecessary here, it'll happen
823 ;; presently.
824 (context-coloring-forward-sws)
825 (context-coloring-get-syntax-code)))
826 dispatch-function)
827 ;; Figure out if the sexp is a special form.
828 (cond
829 ((and (context-coloring-elisp-identifier-p syntax-code)
830 (setq dispatch-function (gethash
831 (buffer-substring-no-properties
832 (point)
833 (progn (forward-sexp)
834 (point)))
835 context-coloring-elisp-callee-dispatch-hash-table)))
836 (goto-char start)
837 (funcall dispatch-function))
838 ;; Not a special form; just colorize the remaining region.
839 (t
840 (context-coloring-colorize-region
841 start
842 end
843 (context-coloring-elisp-get-current-scope-level))
844 (context-coloring-elisp-colorize-region (point) (1- end))
845 (forward-char)))))
846
847 (defun context-coloring-elisp-colorize-symbol ()
848 "Color the symbol at point."
849 (context-coloring-elisp-increment-sexp-count)
850 (let* ((symbol-pos (point))
851 (symbol-end (progn (forward-sexp)
852 (point)))
853 (symbol-string (buffer-substring-no-properties
854 symbol-pos
855 symbol-end)))
856 (cond
857 ((string-match-p context-coloring-elisp-ignored-word-regexp symbol-string))
858 (t
859 (context-coloring-colorize-region
860 symbol-pos
861 symbol-end
862 (context-coloring-elisp-get-variable-level
863 symbol-string))))))
864
865 (defun context-coloring-elisp-colorize-backquote-form ()
866 "Color the backquote form at point."
867 (let ((start (point))
868 (end (progn (forward-sexp)
869 (point)))
870 char)
871 (goto-char start)
872 (while (> end (progn (forward-char)
873 (point)))
874 (setq char (char-after))
875 (when (= char context-coloring-COMMA-CHAR)
876 (forward-char)
877 (when (= (char-after) context-coloring-AT-CHAR)
878 ;; If we don't do this "@" could be interpreted as a symbol.
879 (forward-char))
880 (context-coloring-elisp-forward-sws)
881 (context-coloring-elisp-colorize-sexp)))
882 ;; We could probably do this as part of the above loop but it'd be
883 ;; repetitive.
884 (context-coloring-elisp-colorize-comments-and-strings-in-region
885 start end)))
886
887 (defun context-coloring-elisp-colorize-backquote ()
888 "Color the `backquote' at point."
889 (context-coloring-elisp-skip-callee-name)
890 (context-coloring-elisp-colorize-backquote-form)
891 ;; Exit.
892 (forward-char))
893
894 (defun context-coloring-elisp-colorize-expression-prefix ()
895 "Color the expression prefix and the following expression at
896 point. It could be a quoted or backquoted expression."
897 (context-coloring-elisp-increment-sexp-count)
898 (cond
899 ((/= (char-after) context-coloring-BACKTICK-CHAR)
900 (context-coloring-elisp-forward-sexp))
901 (t
902 (context-coloring-elisp-colorize-backquote-form))))
903
904 (defun context-coloring-elisp-colorize-comment ()
905 "Color the comment at point."
906 (context-coloring-elisp-increment-sexp-count)
907 (context-coloring-elisp-forward-sws))
908
909 (defun context-coloring-elisp-colorize-string ()
910 "Color the string at point."
911 (context-coloring-elisp-increment-sexp-count)
912 (let ((start (point)))
913 (forward-sexp)
914 (context-coloring-colorize-comments-and-strings start (point))))
915
916 ;; Elisp has whitespace, words, symbols, open/close parenthesis, expression
917 ;; prefix, string quote, comment starters/enders and escape syntax classes only.
918
919 (defun context-coloring-elisp-colorize-sexp ()
920 "Color the sexp at point."
921 (let ((syntax-code (context-coloring-get-syntax-code)))
922 (cond
923 ((= syntax-code context-coloring-OPEN-PARENTHESIS-CODE)
924 (context-coloring-elisp-colorize-parenthesized-sexp))
925 ((context-coloring-elisp-identifier-p syntax-code)
926 (context-coloring-elisp-colorize-symbol))
927 ((= syntax-code context-coloring-EXPRESSION-PREFIX-CODE)
928 (context-coloring-elisp-colorize-expression-prefix))
929 ((= syntax-code context-coloring-STRING-QUOTE-CODE)
930 (context-coloring-elisp-colorize-string))
931 ((= syntax-code context-coloring-ESCAPE-CODE)
932 (forward-char 2)))))
933
934 (defun context-coloring-elisp-colorize-comments-and-strings-in-region (start end)
935 "Color comments and strings between START and END."
936 (let (syntax-code)
937 (goto-char start)
938 (while (> end (progn (skip-syntax-forward "^\"<\\" end)
939 (point)))
940 (setq syntax-code (context-coloring-get-syntax-code))
941 (cond
942 ((= syntax-code context-coloring-STRING-QUOTE-CODE)
943 (context-coloring-elisp-colorize-string))
944 ((= syntax-code context-coloring-COMMENT-START-CODE)
945 (context-coloring-elisp-colorize-comment))
946 ((= syntax-code context-coloring-ESCAPE-CODE)
947 (forward-char 2))))))
948
949 (defun context-coloring-elisp-colorize-region (start end)
950 "Color everything between START and END."
951 (let (syntax-code)
952 (goto-char start)
953 (while (> end (progn (skip-syntax-forward "^w_('\"<\\" end)
954 (point)))
955 (setq syntax-code (context-coloring-get-syntax-code))
956 (cond
957 ((= syntax-code context-coloring-OPEN-PARENTHESIS-CODE)
958 (context-coloring-elisp-colorize-parenthesized-sexp))
959 ((context-coloring-elisp-identifier-p syntax-code)
960 (context-coloring-elisp-colorize-symbol))
961 ((= syntax-code context-coloring-EXPRESSION-PREFIX-CODE)
962 (context-coloring-elisp-colorize-expression-prefix))
963 ((= syntax-code context-coloring-STRING-QUOTE-CODE)
964 (context-coloring-elisp-colorize-string))
965 ((= syntax-code context-coloring-COMMENT-START-CODE)
966 (context-coloring-elisp-colorize-comment))
967 ((= syntax-code context-coloring-ESCAPE-CODE)
968 (forward-char 2))))))
969
970 (defun context-coloring-elisp-colorize-region-initially (start end)
971 "Begin coloring everything between START and END."
972 (setq context-coloring-elisp-sexp-count 0)
973 (setq context-coloring-elisp-scope-stack '())
974 (let ((inhibit-point-motion-hooks t)
975 (case-fold-search nil)
976 ;; This is a recursive-descent parser, so give it a big stack.
977 (max-lisp-eval-depth (max max-lisp-eval-depth 3000))
978 (max-specpdl-size (max max-specpdl-size 3000)))
979 (context-coloring-elisp-colorize-region start end)))
980
981 (defun context-coloring-elisp-colorize-guard (callback)
982 "Silently color in CALLBACK."
983 (with-silent-modifications
984 (save-excursion
985 (condition-case nil
986 (funcall callback)
987 ;; Scan errors can happen virtually anywhere if parenthesis are
988 ;; unbalanced. Just swallow them. (`progn' for test coverage.)
989 (scan-error (progn))))))
990
991 (defun context-coloring-elisp-colorize ()
992 "Color the current buffer, parsing elisp to determine its
993 scopes and variables."
994 (interactive)
995 (context-coloring-elisp-colorize-guard
996 (lambda ()
997 (cond
998 ;; Just colorize the changed region.
999 (context-coloring-changed-p
1000 (let* ( ;; Prevent `beginning-of-defun' from making poor assumptions.
1001 (open-paren-in-column-0-is-defun-start nil)
1002 ;; Seek the beginning and end of the previous and next
1003 ;; offscreen defuns, so just enough is colored.
1004 (start (progn (goto-char context-coloring-changed-start)
1005 (while (and (< (point-min) (point))
1006 (pos-visible-in-window-p))
1007 (end-of-line 0))
1008 (beginning-of-defun)
1009 (point)))
1010 (end (progn (goto-char context-coloring-changed-end)
1011 (while (and (> (point-max) (point))
1012 (pos-visible-in-window-p))
1013 (forward-line 1))
1014 (end-of-defun)
1015 (point))))
1016 (context-coloring-elisp-colorize-region-initially start end)
1017 ;; Fast coloring is nice, but if the code is not well-formed
1018 ;; (e.g. an unclosed string literal is parsed at any time) then
1019 ;; there could be leftover incorrectly-colored code offscreen. So
1020 ;; do a clean sweep as soon as appropriate.
1021 (context-coloring-schedule-coloring context-coloring-default-delay)))
1022 (t
1023 (context-coloring-elisp-colorize-region-initially (point-min) (point-max)))))))
1024
1025
1026 ;;; eval-expression colorization
1027
1028 (defun context-coloring-eval-expression-colorize ()
1029 "Color the `eval-expression' minibuffer prompt as elisp."
1030 (interactive)
1031 (context-coloring-elisp-colorize-guard
1032 (lambda ()
1033 (context-coloring-elisp-colorize-region-initially
1034 (progn
1035 (string-match "\\`Eval: " (buffer-string))
1036 (1+ (match-end 0)))
1037 (point-max)))))
1038
1039
1040 ;;; Dispatch
1041
1042 (defvar context-coloring-dispatch-hash-table (make-hash-table :test #'eq)
1043 "Map dispatch strategy names to their corresponding property
1044 lists, which contain details about the strategies.")
1045
1046 (defvar context-coloring-mode-hash-table (make-hash-table :test #'eq)
1047 "Map major mode names to dispatch property lists.")
1048
1049 (defvar context-coloring-dispatch-predicates '()
1050 "Functions which may return a dispatch.")
1051
1052 (defun context-coloring-get-current-dispatch ()
1053 "Return the first dispatch appropriate for the current state."
1054 (let ((predicates context-coloring-dispatch-predicates)
1055 (parent major-mode)
1056 dispatch)
1057 ;; Maybe a predicate will be satisfied and return a dispatch.
1058 (while (and predicates
1059 (not (setq dispatch (funcall (pop predicates))))))
1060 ;; If not, maybe a major mode (or a derivative) will define a dispatch.
1061 (when (not dispatch)
1062 (while (and parent
1063 (not (setq dispatch (gethash parent context-coloring-mode-hash-table)))
1064 (setq parent (get parent 'derived-mode-parent)))))
1065 dispatch))
1066
1067 (defun context-coloring-define-dispatch (symbol &rest properties)
1068 "Define a new dispatch named SYMBOL with PROPERTIES.
1069
1070 A \"dispatch\" is a property list describing a strategy for
1071 coloring a buffer.
1072
1073 PROPERTIES must include one of `:modes' or `:predicate', and a
1074 `:colorizer'.
1075
1076 `:modes' - List of major modes this dispatch is valid for.
1077
1078 `:predicate' - Function that determines if the dispatch is valid
1079 for any given state.
1080
1081 `:colorizer' - Function that parses and colors the buffer.
1082
1083 `:delay' - Delay between buffer update and colorization, to
1084 override `context-coloring-default-delay'.
1085
1086 `:setup' - Arbitrary code to set up this dispatch when
1087 `context-coloring-mode' is enabled.
1088
1089 `:teardown' - Arbitrary code to tear down this dispatch when
1090 `context-coloring-mode' is disabled."
1091 (let ((modes (plist-get properties :modes))
1092 (predicate (plist-get properties :predicate))
1093 (colorizer (plist-get properties :colorizer)))
1094 (when (null (or modes predicate))
1095 (error "No mode or predicate defined for dispatch"))
1096 (when (not colorizer)
1097 (error "No colorizer defined for dispatch"))
1098 (puthash symbol properties context-coloring-dispatch-hash-table)
1099 (dolist (mode modes)
1100 (puthash mode properties context-coloring-mode-hash-table))
1101 (when predicate
1102 (push (lambda ()
1103 (when (funcall predicate)
1104 properties)) context-coloring-dispatch-predicates))))
1105
1106 (defun context-coloring-dispatch ()
1107 "Determine the optimal track for scopification / coloring of
1108 the current buffer, then execute it."
1109 (let* ((dispatch (context-coloring-get-current-dispatch))
1110 (colorizer (plist-get dispatch :colorizer)))
1111 (catch 'interrupted
1112 (funcall colorizer))))
1113
1114
1115 ;;; Colorization
1116
1117 (defun context-coloring-colorize ()
1118 "Color the current buffer by function context."
1119 (interactive)
1120 (context-coloring-dispatch))
1121
1122 (defun context-coloring-colorize-with-buffer (buffer)
1123 "Color BUFFER."
1124 ;; Don't select deleted buffers.
1125 (when (get-buffer buffer)
1126 (with-current-buffer buffer
1127 (context-coloring-colorize))))
1128
1129
1130 ;;; Themes
1131
1132 (defvar context-coloring-theme-hash-table (make-hash-table :test #'eq)
1133 "Map theme names to theme properties.")
1134
1135 (defun context-coloring-theme-p (theme)
1136 "Return t if THEME is defined, nil otherwise."
1137 (and (gethash theme context-coloring-theme-hash-table)))
1138
1139 (defconst context-coloring-level-face-regexp
1140 "context-coloring-level-\\([[:digit:]]+\\)-face"
1141 "Extract a level from a face.")
1142
1143 (defvar context-coloring-originally-set-theme-hash-table
1144 (make-hash-table :test #'eq)
1145 "Cache custom themes who originally set their own
1146 `context-coloring-level-N-face' faces.")
1147
1148 (defun context-coloring-theme-originally-set-p (theme)
1149 "Return t if there is a `context-coloring-level-N-face'
1150 originally set for THEME, nil otherwise."
1151 (let (originally-set)
1152 (cond
1153 ;; `setq' might return a non-nil value for the sake of this `cond'.
1154 ((setq
1155 originally-set
1156 (gethash
1157 theme
1158 context-coloring-originally-set-theme-hash-table))
1159 (eq originally-set 'yes))
1160 (t
1161 (let* ((settings (get theme 'theme-settings))
1162 (tail settings)
1163 found)
1164 (while (and tail (not found))
1165 (and (eq (nth 0 (car tail)) 'theme-face)
1166 (string-match
1167 context-coloring-level-face-regexp
1168 (symbol-name (nth 1 (car tail))))
1169 (setq found t))
1170 (setq tail (cdr tail)))
1171 found)))))
1172
1173 (defun context-coloring-cache-originally-set (theme originally-set)
1174 "Remember if THEME had colors originally set for it. If
1175 ORIGINALLY-SET is non-nil, it did, otherwise it didn't."
1176 ;; Caching whether a theme was originally set is kind of dirty, but we have to
1177 ;; do it to remember the past state of the theme. There are probably some
1178 ;; edge cases where caching will be an issue, but they are probably rare.
1179 (puthash
1180 theme
1181 (if originally-set 'yes 'no)
1182 context-coloring-originally-set-theme-hash-table))
1183
1184 (defun context-coloring-warn-theme-originally-set (theme)
1185 "Warn the user that the colors for THEME are already originally
1186 set."
1187 (warn "Context coloring colors for theme `%s' are already defined" theme))
1188
1189 (defun context-coloring-theme-highest-level (theme)
1190 "Return the highest level N of a face like
1191 `context-coloring-level-N-face' set for THEME, or `-1' if there
1192 is none."
1193 (let* ((settings (get theme 'theme-settings))
1194 (tail settings)
1195 face-string
1196 number
1197 (found -1))
1198 (while tail
1199 (and (eq (nth 0 (car tail)) 'theme-face)
1200 (setq face-string (symbol-name (nth 1 (car tail))))
1201 (string-match
1202 context-coloring-level-face-regexp
1203 face-string)
1204 (setq number (string-to-number
1205 (substring face-string
1206 (match-beginning 1)
1207 (match-end 1))))
1208 (> number found)
1209 (setq found number))
1210 (setq tail (cdr tail)))
1211 found))
1212
1213 (defun context-coloring-apply-theme (theme)
1214 "Apply THEME's properties to its respective custom theme,
1215 which must already exist and which *should* already be enabled."
1216 (let* ((properties (gethash theme context-coloring-theme-hash-table))
1217 (colors (plist-get properties :colors))
1218 (level -1))
1219 ;; Only clobber when we have to.
1220 (when (custom-theme-enabled-p theme)
1221 (setq context-coloring-maximum-face (- (length colors) 1)))
1222 (apply
1223 #'custom-theme-set-faces
1224 theme
1225 (mapcar
1226 (lambda (color)
1227 (setq level (+ level 1))
1228 `(,(context-coloring-level-face level) ((t (:foreground ,color)))))
1229 colors))))
1230
1231 (defun context-coloring-define-theme (theme &rest properties)
1232 "Define a context theme named THEME for coloring scope levels.
1233
1234 PROPERTIES is a property list specifiying the following details:
1235
1236 `:aliases': List of symbols of other custom themes that these
1237 colors are applicable to.
1238
1239 `:colors': List of colors that this context theme uses.
1240
1241 `:override': If non-nil, this context theme is intentionally
1242 overriding colors set by a custom theme. Don't set this non-nil
1243 unless there is a custom theme you want to use which sets
1244 `context-coloring-level-N-face' faces that you want to replace.
1245
1246 `:recede': If non-nil, this context theme should not apply its
1247 colors if a custom theme already sets
1248 `context-coloring-level-N-face' faces. This option is
1249 optimistic; set this non-nil if you would rather confer the duty
1250 of picking colors to a custom theme author (if / when he ever
1251 gets around to it).
1252
1253 By default, context themes will always override custom themes,
1254 even if those custom themes set `context-coloring-level-N-face'
1255 faces. If a context theme does override a custom theme, a
1256 warning will be raised, at which point you may want to enable the
1257 `:override' option, or just delete your context theme and opt to
1258 use your custom theme's author's colors instead.
1259
1260 Context themes only work for the custom theme with the highest
1261 precedence, i.e. the car of `custom-enabled-themes'."
1262 (let ((aliases (plist-get properties :aliases))
1263 (override (plist-get properties :override))
1264 (recede (plist-get properties :recede)))
1265 (dolist (name (append `(,theme) aliases))
1266 (puthash name properties context-coloring-theme-hash-table)
1267 (when (custom-theme-p name)
1268 (let ((originally-set (context-coloring-theme-originally-set-p name)))
1269 (context-coloring-cache-originally-set name originally-set)
1270 ;; In the particular case when you innocently define colors that a
1271 ;; custom theme originally set, warn. Arguably this only has to be
1272 ;; done at enable time, but it is probably more useful to do it at
1273 ;; definition time for prompter feedback.
1274 (when (and originally-set
1275 (not recede)
1276 (not override))
1277 (context-coloring-warn-theme-originally-set name))
1278 ;; Set (or overwrite) colors.
1279 (when (not (and originally-set
1280 recede))
1281 (context-coloring-apply-theme name)))))))
1282
1283 (defun context-coloring-enable-theme (theme)
1284 "Apply THEME if its colors are not already set, else just set
1285 `context-coloring-maximum-face' to the correct value for THEME."
1286 (let* ((properties (gethash theme context-coloring-theme-hash-table))
1287 (recede (plist-get properties :recede))
1288 (override (plist-get properties :override)))
1289 (cond
1290 (recede
1291 (let ((highest-level (context-coloring-theme-highest-level theme)))
1292 (cond
1293 ;; This can be true whether originally set by a custom theme or by a
1294 ;; context theme.
1295 ((> highest-level -1)
1296 (setq context-coloring-maximum-face highest-level))
1297 ;; It is possible that the corresponding custom theme did not exist at
1298 ;; the time of defining this context theme, and in that case the above
1299 ;; condition proves the custom theme did not originally set any faces,
1300 ;; so we have license to apply the context theme for the first time
1301 ;; here.
1302 (t
1303 (context-coloring-apply-theme theme)))))
1304 (t
1305 (let ((originally-set (context-coloring-theme-originally-set-p theme)))
1306 ;; Cache now in case the context theme was defined after the custom
1307 ;; theme.
1308 (context-coloring-cache-originally-set theme originally-set)
1309 (when (and originally-set
1310 (not override))
1311 (context-coloring-warn-theme-originally-set theme))
1312 (context-coloring-apply-theme theme))))))
1313
1314 (defadvice enable-theme (after context-coloring-enable-theme (theme) activate)
1315 "Enable colors for context themes just-in-time."
1316 (when (and (not (eq theme 'user)) ; Called internally by `enable-theme'.
1317 (custom-theme-p theme) ; Guard against non-existent themes.
1318 (context-coloring-theme-p theme))
1319 (when (= (length custom-enabled-themes) 1)
1320 ;; Cache because we can't reliably figure it out in reverse.
1321 (setq context-coloring-original-maximum-face
1322 context-coloring-maximum-face))
1323 (context-coloring-enable-theme theme)))
1324
1325 (defadvice disable-theme (after context-coloring-disable-theme (theme) activate)
1326 "Update `context-coloring-maximum-face'."
1327 (when (custom-theme-p theme) ; Guard against non-existent themes.
1328 (let ((enabled-theme (car custom-enabled-themes)))
1329 (cond
1330 ((context-coloring-theme-p enabled-theme)
1331 (context-coloring-enable-theme enabled-theme))
1332 (t
1333 ;; Assume we are back to no theme; act as if nothing ever happened.
1334 ;; This is still prone to intervention, but rather extraordinarily.
1335 (setq context-coloring-maximum-face
1336 context-coloring-original-maximum-face))))))
1337
1338 (context-coloring-define-theme
1339 'ample
1340 :recede t
1341 :colors '("#bdbdb3"
1342 "#baba36"
1343 "#6aaf50"
1344 "#5180b3"
1345 "#ab75c3"
1346 "#cd7542"
1347 "#df9522"
1348 "#454545"))
1349
1350 (context-coloring-define-theme
1351 'anti-zenburn
1352 :recede t
1353 :colors '("#232333"
1354 "#6c1f1c"
1355 "#401440"
1356 "#0f2050"
1357 "#205070"
1358 "#336c6c"
1359 "#23733c"
1360 "#6b400c"
1361 "#603a60"
1362 "#2f4070"
1363 "#235c5c"))
1364
1365 (context-coloring-define-theme
1366 'grandshell
1367 :recede t
1368 :colors '("#bebebe"
1369 "#5af2ee"
1370 "#b2baf6"
1371 "#f09fff"
1372 "#efc334"
1373 "#f6df92"
1374 "#acfb5a"
1375 "#888888"))
1376
1377 (context-coloring-define-theme
1378 'leuven
1379 :recede t
1380 :colors '("#333333"
1381 "#0000ff"
1382 "#6434a3"
1383 "#ba36a5"
1384 "#d0372d"
1385 "#036a07"
1386 "#006699"
1387 "#006fe0"
1388 "#808080"))
1389
1390 (context-coloring-define-theme
1391 'monokai
1392 :recede t
1393 :colors '("#f8f8f2"
1394 "#66d9ef"
1395 "#a1efe4"
1396 "#a6e22e"
1397 "#e6db74"
1398 "#fd971f"
1399 "#f92672"
1400 "#fd5ff0"
1401 "#ae81ff"))
1402
1403 (context-coloring-define-theme
1404 'solarized
1405 :recede t
1406 :aliases '(solarized-light
1407 solarized-dark
1408 sanityinc-solarized-light
1409 sanityinc-solarized-dark)
1410 :colors '("#839496"
1411 "#268bd2"
1412 "#2aa198"
1413 "#859900"
1414 "#b58900"
1415 "#cb4b16"
1416 "#dc322f"
1417 "#d33682"
1418 "#6c71c4"
1419 "#69b7f0"
1420 "#69cabf"
1421 "#b4c342"
1422 "#deb542"
1423 "#f2804f"
1424 "#ff6e64"
1425 "#f771ac"
1426 "#9ea0e5"))
1427
1428 (context-coloring-define-theme
1429 'spacegray
1430 :recede t
1431 :colors '("#ffffff"
1432 "#89aaeb"
1433 "#c189eb"
1434 "#bf616a"
1435 "#dca432"
1436 "#ebcb8b"
1437 "#b4eb89"
1438 "#89ebca"))
1439
1440 (context-coloring-define-theme
1441 'tango
1442 :recede t
1443 :colors '("#2e3436"
1444 "#346604"
1445 "#204a87"
1446 "#5c3566"
1447 "#a40000"
1448 "#b35000"
1449 "#c4a000"
1450 "#8ae234"
1451 "#8cc4ff"
1452 "#ad7fa8"
1453 "#ef2929"
1454 "#fcaf3e"
1455 "#fce94f"))
1456
1457 (context-coloring-define-theme
1458 'zenburn
1459 :recede t
1460 :colors '("#dcdccc"
1461 "#93e0e3"
1462 "#bfebbf"
1463 "#f0dfaf"
1464 "#dfaf8f"
1465 "#cc9393"
1466 "#dc8cc3"
1467 "#94bff3"
1468 "#9fc59f"
1469 "#d0bf8f"
1470 "#dca3a3"))
1471
1472
1473 ;;; Built-in dispatches
1474
1475 (context-coloring-define-dispatch
1476 'javascript
1477 :modes '(js2-mode)
1478 :colorizer #'context-coloring-js2-colorize
1479 :setup
1480 (lambda ()
1481 (add-hook 'js2-post-parse-callbacks #'context-coloring-colorize nil t))
1482 :teardown
1483 (lambda ()
1484 (remove-hook 'js2-post-parse-callbacks #'context-coloring-colorize t)))
1485
1486 (context-coloring-define-dispatch
1487 'emacs-lisp
1488 :modes '(emacs-lisp-mode)
1489 :colorizer #'context-coloring-elisp-colorize
1490 :delay 0.016 ;; Thanks to lazy colorization this can be 60 frames per second.
1491 :setup #'context-coloring-setup-idle-change-detection
1492 :teardown #'context-coloring-teardown-idle-change-detection)
1493
1494 ;; `eval-expression-minibuffer-setup-hook' is not available in Emacs 24.3, so
1495 ;; the backwards-compatible recommendation is to use `minibuffer-setup-hook' and
1496 ;; rely on this predicate instead.
1497 (defun context-coloring-eval-expression-predicate ()
1498 "Non-nil if the minibuffer is for `eval-expression'."
1499 (eq this-command 'eval-expression))
1500
1501 (context-coloring-define-dispatch
1502 'eval-expression
1503 :predicate #'context-coloring-eval-expression-predicate
1504 :colorizer #'context-coloring-eval-expression-colorize
1505 :delay 0.016
1506 :setup #'context-coloring-setup-idle-change-detection
1507 :teardown #'context-coloring-teardown-idle-change-detection)
1508
1509
1510 ;;; Minor mode
1511
1512 ;;;###autoload
1513 (define-minor-mode context-coloring-mode
1514 "Toggle contextual code coloring.
1515 With a prefix argument ARG, enable Context Coloring mode if ARG
1516 is positive, and disable it otherwise. If called from Lisp,
1517 enable the mode if ARG is omitted or nil.
1518
1519 Context Coloring mode is a buffer-local minor mode. When
1520 enabled, code is colored by scope. Scopes are colored
1521 hierarchically. Variables referenced from nested scopes retain
1522 the color of their defining scopes. Certain syntax, like
1523 comments and strings, is still colored with `font-lock'.
1524
1525 The entire buffer is colored initially. Changes to the buffer
1526 trigger recoloring.
1527
1528 Certain custom themes have predefined colors from their palettes
1529 to use for coloring. See `context-coloring-theme-hash-table' for
1530 the supported themes. If the currently-enabled custom theme is
1531 not among these, you can define colors for it with
1532 `context-coloring-define-theme', which see.
1533
1534 New language / major mode support can be added with
1535 `context-coloring-define-dispatch', which see.
1536
1537 Feature inspired by Douglas Crockford."
1538 nil " Context" nil
1539 (cond
1540 (context-coloring-mode
1541 ;; Font lock is incompatible with this mode; the converse is also true.
1542 (font-lock-mode 0)
1543 (jit-lock-mode nil)
1544 ;; ...but we do use font-lock functions here.
1545 (font-lock-set-defaults)
1546 ;; Safely change the value of this function as necessary.
1547 (make-local-variable 'font-lock-syntactic-face-function)
1548 (let ((dispatch (context-coloring-get-current-dispatch)))
1549 (cond
1550 (dispatch
1551 (let ((setup (plist-get dispatch :setup)))
1552 (when setup
1553 (funcall setup))
1554 ;; Colorize once initially.
1555 (let ((context-coloring-parse-interruptable-p nil))
1556 (context-coloring-colorize))))
1557 (t
1558 (message "Context coloring is not available for this major mode")))))
1559 (t
1560 (let ((dispatch (context-coloring-get-current-dispatch)))
1561 (when dispatch
1562 (let ((teardown (plist-get dispatch :teardown)))
1563 (when teardown
1564 (funcall teardown)))))
1565 (font-lock-mode)
1566 (jit-lock-mode t))))
1567
1568 (provide 'context-coloring)
1569
1570 ;;; context-coloring.el ends here