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