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