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