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