]> code.delx.au - gnu-emacs-elpa/commitdiff
Merge commit '6f3ad757155b9b3089aba55ee6102ecc9bed647d' from context-coloring
authorJackson Ray Hamilton <jackson@jacksonrayhamilton.com>
Mon, 9 Feb 2015 01:07:07 +0000 (17:07 -0800)
committerJackson Ray Hamilton <jackson@jacksonrayhamilton.com>
Mon, 9 Feb 2015 01:07:07 +0000 (17:07 -0800)
1  2 
packages/context-coloring/README.md
packages/context-coloring/context-coloring.el
packages/context-coloring/test/context-coloring-test.el

index 21ba1846236ad8e0c68752098e203e61e9265779,ff305c1533e6f19b4c1dc0c3b99a808fcda159c5..ff305c1533e6f19b4c1dc0c3b99a808fcda159c5
@@@ -90,8 -90,9 +90,9 @@@ Add the following to your `~/.emacs` fi
  ## Customizing
  
  Color schemes for custom themes are automatically applied when those themes are
- active. Built-in theme support is available for: `leuven`, `monokai`,
- `solarized`, `tango` and `zenburn`.
+ active. Built-in theme support is available for: `ample`, `anti-zenburn`,
+ `grandshell`, `leuven`, `monokai`, `solarized`, `spacegray`, `tango` and
+ `zenburn`.
  
  You can define your own theme colors too:
  
             "#DCA3A3"))
  ```
  
+ See `C-h f context-coloring-define-theme` for more info on theme parameters.
  ## Extending
  
  To add support for a new language, write a "scopifier" for it, and define a new
  coloring dispatch strategy with `context-coloring-define-dispatch`. Then the
- plugin should handle the rest.
+ plugin should handle the rest. (See `C-h f context-coloring-define-dispatch` for
+ more info on dispatch strategies.)
  
  A "scopifier" is a CLI program that reads a buffer's contents from stdin and
  writes a JSON array of numbers to stdout. Every three numbers in the array
@@@ -171,9 -175,7 +175,7 @@@ required
  
  [linter]: http://jshint.com/about/
  [flycheck]: http://www.flycheck.org/
- [zenburn]: http://github.com/bbatsov/zenburn-emacs
  [point]: http://www.gnu.org/software/emacs/manual/html_node/elisp/Point.html
  [js2-mode]: https://github.com/mooz/js2-mode
  [node]: http://nodejs.org/download/
  [scopifier]: https://github.com/jacksonrayhamilton/scopifier
- [load path]: https://www.gnu.org/software/emacs/manual/html_node/emacs/Lisp-Libraries.html
index 6af9444e28e68b4b30006bd8edbc022b64072cf1,6b6ffe9657063a40ff8515459f24862acba7a506..6b6ffe9657063a40ff8515459f24862acba7a506
@@@ -5,7 -5,7 +5,7 @@@
  ;; Author: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
  ;; URL: https://github.com/jacksonrayhamilton/context-coloring
  ;; Keywords: context coloring syntax highlighting
- ;; Version: 4.1.0
+ ;; Version: 5.0.0
  ;; Package-Requires: ((emacs "24") (js2-mode "20150126"))
  
  ;; This file is part of GNU Emacs.
  (require 'js2-mode)
  
  
- ;;; Constants
- (defconst context-coloring-path
-   (file-name-directory (or load-file-name buffer-file-name))
-   "This file's directory.")
  ;;; Customizable options
  
  (defcustom context-coloring-delay 0.25
@@@ -81,8 -74,8 +74,8 @@@ Supported modes: `js-mode', `js3-mode'
  (defcustom context-coloring-js-block-scopes nil
    "If non-nil, also color block scopes in the scope hierarchy in JavaScript.
  
- The block-scope-inducing `let' and `const' are introduced in ES6.
If you are writing ES6 code, enable this; otherwise, don't.
+ The block-scoped `let' and `const' are introduced in ES6.  If you
+ are writing ES6 code, enable this; otherwise, don't.
  
  Supported modes: `js2-mode'"
    :group 'context-coloring)
@@@ -115,23 -108,28 +108,28 @@@ used."
  ;;; Faces
  
  (defun context-coloring-defface (level tty light dark)
+   "Dynamically define a face for LEVEL with colors for TTY, LIGHT
+ and DARK backgrounds."
    (let ((face (intern (format "context-coloring-level-%s-face" level)))
          (doc (format "Context coloring face, level %s." level)))
-     (eval (macroexpand `(defface ,face
-                           '((((type tty)) (:foreground ,tty))
-                             (((background light)) (:foreground ,light))
-                             (((background dark)) (:foreground ,dark)))
-                           ,doc
-                           :group 'context-coloring)))))
+     (eval
+      (macroexpand
+       `(defface ,face
+          '((((type tty)) (:foreground ,tty))
+            (((background light)) (:foreground ,light))
+            (((background dark)) (:foreground ,dark)))
+          ,doc
+          :group 'context-coloring)))))
  
  (defvar context-coloring-face-count nil
-   "Number of faces available for context coloring.")
+   "Number of faces available for coloring.")
  
  (defun context-coloring-defface-default (level)
-   (context-coloring-defface level "white" "#3f3f3f" "#cdcdcd"))
+   "Define a face for LEVEL with the default neutral colors."
+   (context-coloring-defface level nil "#3f3f3f" "#cdcdcd"))
  
  (defun context-coloring-set-colors-default ()
-   (context-coloring-defface 0 "white"   "#000000" "#ffffff")
+   (context-coloring-defface 0 nil       "#000000" "#ffffff")
    (context-coloring-defface 1 "yellow"  "#007f80" "#ffff80")
    (context-coloring-defface 2 "green"   "#001580" "#cdfacd")
    (context-coloring-defface 3 "cyan"    "#550080" "#d8d8ff")
@@@ -292,7 -290,8 +290,8 @@@ element.
  
  (defun context-coloring-parse-array (input)
    "Specialized JSON parser for a flat array of numbers."
-   (vconcat (mapcar 'string-to-number (split-string (substring input 1 -1) ","))))
+   (vconcat
+    (mapcar 'string-to-number (split-string (substring input 1 -1) ","))))
  
  (defun context-coloring-kill-scopifier ()
    "Kills the currently-running scopifier process for this
@@@ -339,8 -338,11 +338,11 @@@ Invokes CALLBACK when complete.
             (if callback (funcall callback)))))))
  
    ;; Give the process its input so it can begin.
-   (process-send-region context-coloring-scopifier-process (point-min) (point-max))
-   (process-send-eof context-coloring-scopifier-process))
+   (process-send-region
+    context-coloring-scopifier-process
+    (point-min) (point-max))
+   (process-send-eof
+    context-coloring-scopifier-process))
  
  
  ;;; Dispatch
@@@ -479,51 -481,243 +481,243 @@@ would be redundant.
  (defvar context-coloring-theme-hash-table (make-hash-table :test 'eq)
    "Mapping of theme names to theme properties.")
  
+ (defun context-coloring-theme-p (theme)
+   "Return t if THEME is defined, nil otherwise."
+   (and (gethash theme context-coloring-theme-hash-table)))
+ (defconst context-coloring-level-face-regexp
+   "context-coloring-level-\\([[:digit:]]+\\)-face"
+   "Regular expression for extracting a level from a face.")
+ (defvar context-coloring-originally-set-theme-hash-table
+   (make-hash-table :test 'eq)
+   "Cache of custom themes who originally set their own
+   `context-coloring-level-N-face' faces.")
+ (defun context-coloring-theme-originally-set-p (theme)
+   "Return t if there is a `context-coloring-level-N-face'
+ originally set for THEME, nil otherwise."
+   (let (originally-set)
+     (cond
+      ;; `setq' might return a non-nil value for the sake of this `cond'.
+      ((setq
+        originally-set
+        (gethash
+         theme
+         context-coloring-originally-set-theme-hash-table))
+       (eq originally-set 'yes))
+      (t
+       (let* ((settings (get theme 'theme-settings))
+              (tail settings)
+              found)
+         (while (and tail (not found))
+           (and (eq (nth 0 (car tail)) 'theme-face)
+                (string-match
+                 context-coloring-level-face-regexp
+                 (symbol-name (nth 1 (car tail))))
+                (setq found t))
+           (setq tail (cdr tail)))
+         found)))))
+ (defun context-coloring-cache-originally-set (theme originally-set)
+   "Remember if THEME had colors originally set for it; if
+ ORIGINALLY-SET is non-nil, it did, otherwise it didn't."
+   ;; Caching whether a theme was originally set is kind of dirty, but we have to
+   ;; do it to remember the past state of the theme.  There are probably some
+   ;; edge cases where caching will be an issue, but they are probably rare.
+   (puthash
+    theme
+    (if originally-set 'yes 'no)
+    context-coloring-originally-set-theme-hash-table))
+ (defun context-coloring-warn-theme-originally-set (theme)
+   "Warns the user that the colors for a theme are already
+ originally set."
+   (warn "Context coloring colors for theme `%s' are already defined" theme))
+ (defun context-coloring-theme-highest-level (theme)
+   "Return the highest level N of a face like
+ `context-coloring-level-N-face' set for THEME, or -1 if there is
+ none."
+   (let* ((settings (get theme 'theme-settings))
+          (tail settings)
+          face-string
+          number
+          (found -1))
+     (while tail
+       (and (eq (nth 0 (car tail)) 'theme-face)
+            (setq face-string (symbol-name (nth 1 (car tail))))
+            (string-match
+             context-coloring-level-face-regexp
+             face-string)
+            (setq number (string-to-number
+                          (substring face-string
+                                     (match-beginning 1)
+                                     (match-end 1))))
+            (> number found)
+            (setq found number))
+       (setq tail (cdr tail)))
+     found))
  (defun context-coloring-apply-theme (theme)
    "Applies THEME's properties to its respective custom theme,
  which must already exist and which *should* already be enabled."
-   (let ((properties (gethash theme context-coloring-theme-hash-table)))
-     (when (null properties)
-       (error (format "No such theme `%s'" theme)))
-     (let ((colors (plist-get properties :colors)))
-       (setq context-coloring-face-count (length colors)) ; Side-effect?
-       (let ((level -1))
-         ;; AFAIK, no way to know if a theme already has a face set, so just
-         ;; override blindly for now.
-         (apply
-          'custom-theme-set-faces
-          theme
-          (mapcar
-           (lambda (color)
-             (setq level (+ level 1))
-             `(,(context-coloring-face-symbol level) ((t (:foreground ,color)))))
-           colors))))))
+   (let* ((properties (gethash theme context-coloring-theme-hash-table))
+          (colors (plist-get properties :colors))
+          (level -1))
+     (setq context-coloring-face-count (length colors))
+     (apply
+      'custom-theme-set-faces
+      theme
+      (mapcar
+       (lambda (color)
+         (setq level (+ level 1))
+         `(,(context-coloring-face-symbol level) ((t (:foreground ,color)))))
+       colors))))
  
  (defun context-coloring-define-theme (theme &rest properties)
-   "Define a theme named THEME for coloring scope levels.
+   "Define a context theme named THEME for coloring scope levels.
  PROPERTIES is a property list specifiying the following details:
  
- `:colors': List of colors that this theme uses."
-   (let ((aliases (plist-get properties :aliases)))
+ `:aliases': List of symbols of other custom themes that these
+ colors are applicable to.
+ `:colors': List of colors that this context theme uses.
+ `:override': If non-nil, this context theme is intentionally
+ overriding colors set by a custom theme.  Don't set this non-nil
+ unless there is a custom theme you want to use which sets
+ `context-coloring-level-N-face' faces that you want to replace.
+ `:recede': If non-nil, this context theme should not apply its
+ colors if a custom theme already sets
+ `context-coloring-level-N-face' faces.  This option is
+ optimistic; set this non-nil if you would rather confer the duty
+ of picking colors to a custom theme author (if / when he ever
+ gets around to it).
+ By default, context themes will always override custom themes,
+ even if those custom themes set `context-coloring-level-N-face'
+ faces.  If a context theme does override a custom theme, a
+ warning will be raised, at which point you may want to enable the
+ `:override' option, or just delete your context theme and opt to
+ use your custom theme's author's colors instead.
+ Context themes only work for the custom theme with the highest
+ precedence, i.e. the car of `custom-enabled-themes'."
+   (let ((aliases (plist-get properties :aliases))
+         (override (plist-get properties :override))
+         (recede (plist-get properties :recede)))
      (dolist (name (append `(,theme) aliases))
        (puthash name properties context-coloring-theme-hash-table)
-       ;; Compensate for already-enabled themes by applying their colors now.
-       (when (custom-theme-enabled-p name)
-         (context-coloring-apply-theme name)))))
- (defun context-coloring-load-theme (&optional rest)
-   (declare (obsolete
-             "themes are now loaded alongside custom themes automatically."
-             "4.1.0")))
+       (when (custom-theme-p name)
+         (let ((originally-set (context-coloring-theme-originally-set-p name)))
+           (context-coloring-cache-originally-set name originally-set)
+           ;; In the particular case when you innocently define colors that a
+           ;; custom theme originally set, warn.  Arguably this only has to be
+           ;; done at enable time, but it is probably more useful to do it at
+           ;; definition time for prompter feedback.
+           (when (and originally-set
+                      (not recede)
+                      (not override))
+             (context-coloring-warn-theme-originally-set name))
+           ;; Set (or overwrite) colors.
+           (when (not (and originally-set
+                           recede))
+             (context-coloring-apply-theme name)))))))
+ (defun context-coloring-enable-theme (theme)
+   "Applies THEME if its colors are not already set, else just
+ sets `context-coloring-face-count' to the correct value for
+ THEME."
+   (let* ((properties (gethash theme context-coloring-theme-hash-table))
+          (recede (plist-get properties :recede))
+          (override (plist-get properties :override)))
+     (cond
+      (recede
+       (let ((highest-level (context-coloring-theme-highest-level theme)))
+         (cond
+          ;; This can be true whether originally set by a custom theme or by a
+          ;; context theme.
+          ((> highest-level -1)
+           (setq context-coloring-face-count (+ highest-level 1)))
+          ;; It is possible that the corresponding custom theme did not exist at
+          ;; the time of defining this context theme, and in that case the above
+          ;; condition proves the custom theme did not originally set any faces,
+          ;; so we have license to apply the context theme for the first time
+          ;; here.
+          (t
+           (context-coloring-apply-theme theme)))))
+      (t
+       (let ((originally-set (context-coloring-theme-originally-set-p theme)))
+         ;; Cache now in case the context theme was defined after the custom
+         ;; theme.
+         (context-coloring-cache-originally-set theme originally-set)
+         (when (and originally-set
+                    (not override))
+           (context-coloring-warn-theme-originally-set theme))
+         (context-coloring-apply-theme theme))))))
  
  (defadvice enable-theme (after context-coloring-enable-theme (theme) activate)
-   "Add colors to themes just-in-time."
-   (when (and (not (eq theme 'user))  ; Called internally.
-              (custom-theme-p theme)) ; Guard against non-existent themes.
-     (context-coloring-apply-theme theme)))
+   "Enable colors for context themes just-in-time.  We can't set
+ faces for custom themes that might not exist yet."
+   (when (and (not (eq theme 'user)) ; Called internally by `enable-theme'.
+              (custom-theme-p theme) ; Guard against non-existent themes.
+              (context-coloring-theme-p theme))
+     (context-coloring-enable-theme theme)))
+ (defadvice disable-theme (after context-coloring-disable-theme (theme) activate)
+   "Colors are disabled normally, but
+ `context-coloring-face-count' isn't.  Update it here."
+   (when (custom-theme-p theme) ; Guard against non-existent themes.
+     (let ((enabled-theme (car custom-enabled-themes)))
+       (if (context-coloring-theme-p enabled-theme)
+           (context-coloring-enable-theme enabled-theme)
+         (context-coloring-set-colors-default)))))
+ (context-coloring-define-theme
+  'ample
+  :recede t
+  :colors '("#bdbdb3"
+            "#baba36"
+            "#6aaf50"
+            "#5180b3"
+            "#ab75c3"
+            "#cd7542"
+            "#dF9522"
+            "#454545"))
+ (context-coloring-define-theme
+  'anti-zenburn
+  :recede t
+  :colors '("#232333"
+            "#6c1f1c"
+            "#401440"
+            "#0f2050"
+            "#205070"
+            "#336c6c"
+            "#23733c"
+            "#6b400c"
+            "#603a60"
+            "#2f4070"
+            "#235c5c"))
+ (context-coloring-define-theme
+  'grandshell
+  :recede t
+  :colors '("#bebebe"
+            "#5af2ee"
+            "#b2baf6"
+            "#f09fff"
+            "#efc334"
+            "#f6df92"
+            "#acfb5a"
+            "#888888"))
  
  (context-coloring-define-theme
   'leuven
+  :recede t
   :colors '("#333333"
             "#0000FF"
             "#6434A3"
  
  (context-coloring-define-theme
   'monokai
+  :recede t
   :colors '("#F8F8F2"
             "#66D9EF"
             "#A1EFE4"
  
  (context-coloring-define-theme
   'solarized
+  :recede t
   :aliases '(solarized-light
              solarized-dark
              sanityinc-solarized-light
             "#F771AC"
             "#9EA0E5"))
  
+ (context-coloring-define-theme
+  'spacegray
+  :recede t
+  :colors '("#ffffff"
+            "#89AAEB"
+            "#C189EB"
+            "#bf616a"
+            "#DCA432"
+            "#ebcb8b"
+            "#B4EB89"
+            "#89EBCA"))
  (context-coloring-define-theme
   'tango
+  :recede t
   :colors '("#2e3436"
             "#346604"
             "#204a87"
  
  (context-coloring-define-theme
   'zenburn
+  :recede t
   :colors '("#DCDCCC"
             "#93E0E3"
             "#BFEBBF"
          (context-coloring-kill-scopifier)
          (when context-coloring-colorize-idle-timer
            (cancel-timer context-coloring-colorize-idle-timer))
-         (remove-hook 'js2-post-parse-callbacks 'context-coloring-colorize t)
-         (remove-hook 'after-change-functions 'context-coloring-change-function t)
+         (remove-hook
+          'js2-post-parse-callbacks 'context-coloring-colorize t)
+         (remove-hook
+          'after-change-functions 'context-coloring-change-function t)
          (font-lock-mode)
          (jit-lock-mode t))
  
-     ;; Remember this buffer. This value should not be dynamically-bound.
+     ;; Remember this buffer.  This value should not be dynamically-bound.
      (setq context-coloring-buffer (current-buffer))
  
      ;; Font lock is incompatible with this mode; the converse is also true.
        ;; Only recolor on reparse.
        (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t))
       (t
-       ;; Only recolor on change.
-       (add-hook 'after-change-functions 'context-coloring-change-function nil t)))
-     (when (not (equal major-mode 'js2-mode))
-       ;; Only recolor idly.
+       ;; Only recolor on change, idly.
+       (add-hook 'after-change-functions 'context-coloring-change-function nil t)
        (setq context-coloring-colorize-idle-timer
              (run-with-idle-timer
               context-coloring-delay
               t
-              'context-coloring-maybe-colorize)))))
+              'context-coloring-maybe-colorize))))))
  
  (provide 'context-coloring)
  
index 607882bd077e2a595b5587c7b169f26d126bebf1,fdb0d83cfae0bfd19377cdbd24999727b3549b1b..fdb0d83cfae0bfd19377cdbd24999727b3549b1b
@@@ -19,6 -19,9 +19,9 @@@
  
  ;;; Code:
  
+ (require 'ert-async)
  ;;; Test running utilities
  
  (defconst context-coloring-test-path
@@@ -68,7 -71,8 +71,8 @@@ is done.
                (kill-buffer temp-buffer))
           (set-buffer previous-buffer))))))
  
- (defun context-coloring-test-with-fixture-async (fixture callback &optional setup)
+ (defun context-coloring-test-with-fixture-async
+     (fixture callback &optional setup)
    "Evaluate CALLBACK in a temporary buffer with the relative
  FIXTURE.  A teardown callback is passed to CALLBACK for it to
  invoke when it is done.  An optional SETUP callback can be passed
@@@ -117,7 -121,8 +121,8 @@@ instantiated in SETUP.
  format."
    (let ((test-name (intern (format "context-coloring-test-js-mode-%s" name)))
          (fixture (format "./fixtures/%s.js" name))
-         (function-name (intern-soft (format "context-coloring-test-js-%s" name))))
+         (function-name (intern-soft
+                         (format "context-coloring-test-js-%s" name))))
      `(ert-deftest-async ,test-name (done)
                          (context-coloring-test-js-mode
                           ,fixture
      "Define a test for `js2-mode' in the typical format."
    (let ((test-name (intern (format "context-coloring-test-js2-mode-%s" name)))
          (fixture (format "./fixtures/%s.js" name))
-         (function-name (intern-soft (format "context-coloring-test-js-%s" name))))
+         (function-name (intern-soft
+                         (format "context-coloring-test-js-%s" name))))
      `(ert-deftest ,test-name ()
         (context-coloring-test-js2-mode
          ,fixture
@@@ -153,10 -159,6 +159,6 @@@ region.  Provides the free variables `i
           ,@body)
         (setq i (+ i 1)))))
  
- (defconst context-coloring-test-level-regexp
-   "context-coloring-level-\\([[:digit:]]+\\)-face"
-   "Regular expression for extracting a level from a face.")
  (defun context-coloring-test-assert-region-level (start end level)
    "Assert that all points in the range [START, END) are of level
  LEVEL."
     (when (not (when face
                  (let* ((face-string (symbol-name face))
                         (matches (string-match
-                                  context-coloring-test-level-regexp
+                                  context-coloring-level-face-regexp
                                   face-string)))
                    (when matches
                      (setq actual-level (string-to-number
@@@ -209,32 -211,69 +211,69 @@@ EXPECTED-FACE.
    (context-coloring-test-assert-region-face
     start end 'font-lock-string-face))
  
- (defun context-coloring-test-assert-message (expected)
-   "Assert that the *Messages* buffer has message EXPECTED."
-   (with-current-buffer "*Messages*"
+ (defun context-coloring-test-assert-message (expected buffer)
+   "Assert that BUFFER has message EXPECTED."
+   (when (null (get-buffer buffer))
+     (ert-fail
+      (format
+       (concat
+        "Expected buffer `%s' to have message \"%s\", "
+        "but the buffer did not have any messages.")
+       buffer expected)))
+   (with-current-buffer buffer
      (let ((messages (split-string
                       (buffer-substring-no-properties
                        (point-min)
                        (point-max))
                       "\n")))
        (let ((message (car (nthcdr (- (length messages) 2) messages))))
-         (should (equal message expected))))))
- (defun context-coloring-test-assert-face (level foreground)
+         (when (not (equal message expected))
+           (ert-fail
+            (format
+             (concat
+              "Expected buffer `%s' to have message \"%s\", "
+              "but instead it was \"%s\"")
+             buffer expected
+             message)))))))
+ (defun context-coloring-test-assert-no-message (buffer)
+   "Assert that BUFFER has no message."
+   (when (get-buffer buffer)
+     (ert-fail (format (concat "Expected buffer `%s' to have no messages, "
+                               "but it did: `%s'")
+                       buffer
+                       (with-current-buffer buffer
+                         (buffer-string))))))
+ (defun context-coloring-test-kill-buffer (buffer)
+   "Kill BUFFER if it exists."
+   (if (get-buffer buffer) (kill-buffer buffer)))
+ (defun context-coloring-test-assert-face (level foreground &optional negate)
    "Assert that a face for LEVEL exists and that its `:foreground'
  is FOREGROUND."
    (let* ((face (context-coloring-face-symbol level))
           actual-foreground)
-     (when (not face)
+     (when (not (or negate
+                    face))
        (ert-fail (format (concat "Expected face for level `%s' to exist; "
                                  "but it didn't")
                          level)))
      (setq actual-foreground (face-attribute face :foreground))
-     (when (not (string-equal foreground actual-foreground))
+     (when (funcall (if negate 'identity 'not)
+                    (string-equal foreground actual-foreground))
        (ert-fail (format (concat "Expected face for level `%s' "
-                                 "to have foreground `%s'; but it was `%s'")
+                                 "%sto have foreground `%s'; "
+                                 "but it %s.")
                          level
-                         foreground actual-foreground)))))
+                         (if negate "not " "") foreground
+                         (if negate "did" (format "was `%s'" actual-foreground)))))))
+ (defun context-coloring-test-assert-not-face (&rest arguments)
+   "Assert that LEVEL does not have a face with `:foreground'
+ FOREGROUND."
+   (apply 'context-coloring-test-assert-face
+          (append arguments '(t))))
  
  
  ;;; The tests
     "./fixtures/function-scopes.js"
     (context-coloring-mode)
     (context-coloring-test-assert-message
-     "Context coloring is not available for this major mode")))
+     "Context coloring is not available for this major mode"
+     "*Messages*")))
  
  (ert-deftest context-coloring-test-set-colors ()
    ;; This test has an irreversible side-effect in that it defines faces beyond
    (context-coloring-test-assert-face 8 "#888888")
    (context-coloring-test-assert-face 9 "#999999"))
  
+ (defvar context-coloring-test-theme-index 0
+   "Unique index for unique theme names.")
+ (defun context-coloring-test-get-next-theme ()
+   "Return a unique symbol for a throwaway theme."
+   (prog1
+       (intern (format "context-coloring-test-theme-%s"
+                       context-coloring-test-theme-index))
+     (setq context-coloring-test-theme-index
+           (+ context-coloring-test-theme-index 1))))
+ (defun context-coloring-test-assert-theme-originally-set-p
+     (settings &optional negate)
+   "Assert that `context-coloring-theme-originally-set-p' returns
+ t for a theme with SETTINGS (or the inverse if NEGATE is
+ non-nil)."
+   (let ((theme (context-coloring-test-get-next-theme)))
+     (put theme 'theme-settings settings)
+     (when (funcall (if negate 'identity 'not)
+                    (context-coloring-theme-originally-set-p theme))
+       (ert-fail (format (concat "Expected theme `%s' with settings `%s' "
+                                 "%sto be considered to have defined a level, "
+                                 "but it %s.")
+                         theme settings
+                         (if negate "not " "")
+                         (if negate "was" "wasn't"))))))
+ (defun context-coloring-test-assert-not-theme-originally-set-p (&rest arguments)
+   "Assert that `context-coloring-theme-originally-set-p' does not
+ return t for a theme with SETTINGS."
+   (apply 'context-coloring-test-assert-theme-originally-set-p
+          (append arguments '(t))))
+ (ert-deftest context-coloring-test-theme-originally-set-p ()
+   (context-coloring-test-assert-theme-originally-set-p
+    '((theme-face context-coloring-level-0-face)))
+   (context-coloring-test-assert-theme-originally-set-p
+    '((theme-face face)
+      (theme-face context-coloring-level-0-face)))
+   (context-coloring-test-assert-theme-originally-set-p
+    '((theme-face context-coloring-level-0-face)
+      (theme-face face)))
+   (context-coloring-test-assert-not-theme-originally-set-p
+    '((theme-face face)))
+   )
+ (defun context-coloring-test-assert-theme-settings-highest-level
+     (settings expected-level)
+   "Assert that a theme with SETTINGS has the highest level
+ EXPECTED-LEVEL."
+   (let ((theme (context-coloring-test-get-next-theme)))
+     (put theme 'theme-settings settings)
+     (context-coloring-test-assert-theme-highest-level theme expected-level)))
+ (defun context-coloring-test-assert-theme-highest-level
+     (theme expected-level &optional negate)
+   "Assert that THEME has the highest level EXPECTED-LEVEL."
+   (let ((highest-level (context-coloring-theme-highest-level theme)))
+     (when (funcall (if negate 'identity 'not) (eq highest-level expected-level))
+       (ert-fail (format (concat "Expected theme with settings `%s' "
+                                 "%sto have a highest level of `%s', "
+                                 "but it %s.")
+                         (get theme 'theme-settings)
+                         (if negate "not " "") expected-level
+                         (if negate "did" (format "was %s" highest-level)))))))
+ (defun context-coloring-test-assert-theme-not-highest-level (&rest arguments)
+   "Assert that THEME's highest level is not EXPECTED-LEVEL."
+   (apply 'context-coloring-test-assert-theme-highest-level
+          (append arguments '(t))))
+ (ert-deftest context-coloring-test-theme-highest-level ()
+   (context-coloring-test-assert-theme-settings-highest-level
+    '((theme-face foo))
+    -1)
+   (context-coloring-test-assert-theme-settings-highest-level
+    '((theme-face context-coloring-level-0-face))
+    0)
+   (context-coloring-test-assert-theme-settings-highest-level
+    '((theme-face context-coloring-level-1-face))
+    1)
+   (context-coloring-test-assert-theme-settings-highest-level
+    '((theme-face context-coloring-level-1-face)
+      (theme-face context-coloring-level-0-face))
+    1)
+   (context-coloring-test-assert-theme-settings-highest-level
+    '((theme-face context-coloring-level-0-face)
+      (theme-face context-coloring-level-1-face))
+    1)
+   )
+ (defmacro context-coloring-test-deftest-define-theme (name &rest body)
+   "Define a test with an automatically-generated theme symbol
+ available as a free variable `theme'.  Side-effects from enabling
+ themes are reversed after the test completes."
+   (declare (indent defun))
+   (let ((deftest-name (intern
+                        (format "context-coloring-test-define-theme-%s" name))))
+     `(ert-deftest ,deftest-name ()
+        (context-coloring-test-kill-buffer "*Warnings*")
+        (let ((theme (context-coloring-test-get-next-theme)))
+          (unwind-protect
+              (progn
+                ,@body)
+            ;; Always cleanup.
+            (disable-theme theme)
+            (context-coloring-set-colors-default))))))
+ (defun context-coloring-test-deftheme (theme)
+   "Dynamically define theme THEME."
+   (eval (macroexpand `(deftheme ,theme))))
+ (context-coloring-test-deftest-define-theme additive
+   (context-coloring-test-deftheme theme)
+   (context-coloring-define-theme
+    theme
+    :colors '("#aaaaaa"
+              "#bbbbbb"))
+   (context-coloring-test-assert-no-message "*Warnings*")
+   (enable-theme theme)
+   (context-coloring-test-assert-no-message "*Warnings*")
+   (context-coloring-test-assert-face 0 "#aaaaaa")
+   (context-coloring-test-assert-face 1 "#bbbbbb"))
+ (defun context-coloring-test-assert-defined-warning (theme)
+   "Assert that a warning about colors already being defined for
+ theme THEME is signaled."
+   (context-coloring-test-assert-message
+    (format (concat "Warning (emacs): Context coloring colors for theme "
+                    "`%s' are already defined")
+            theme)
+    "*Warnings*"))
+ (context-coloring-test-deftest-define-theme unintentional-override
+   (context-coloring-test-deftheme theme)
+   (custom-theme-set-faces
+    theme
+    '(context-coloring-level-0-face ((t (:foreground "#aaaaaa"))))
+    '(context-coloring-level-1-face ((t (:foreground "#bbbbbb")))))
+   (context-coloring-define-theme
+    theme
+    :colors '("#cccccc"
+              "#dddddd"))
+   (context-coloring-test-assert-defined-warning theme)
+   (context-coloring-test-kill-buffer "*Warnings*")
+   (enable-theme theme)
+   (context-coloring-test-assert-defined-warning theme)
+   (context-coloring-test-assert-face 0 "#cccccc")
+   (context-coloring-test-assert-face 1 "#dddddd"))
+ (context-coloring-test-deftest-define-theme intentional-override
+   (context-coloring-test-deftheme theme)
+   (custom-theme-set-faces
+    theme
+    '(context-coloring-level-0-face ((t (:foreground "#aaaaaa"))))
+    '(context-coloring-level-1-face ((t (:foreground "#bbbbbb")))))
+   (context-coloring-define-theme
+    theme
+    :override t
+    :colors '("#cccccc"
+              "#dddddd"))
+   (context-coloring-test-assert-no-message "*Warnings*")
+   (enable-theme theme)
+   (context-coloring-test-assert-no-message "*Warnings*")
+   (context-coloring-test-assert-face 0 "#cccccc")
+   (context-coloring-test-assert-face 1 "#dddddd"))
+ (context-coloring-test-deftest-define-theme pre-recede
+   (context-coloring-define-theme
+    theme
+    :recede t
+    :colors '("#aaaaaa"
+              "#bbbbbb"))
+   (context-coloring-test-deftheme theme)
+   (custom-theme-set-faces
+    theme
+    '(context-coloring-level-0-face ((t (:foreground "#cccccc"))))
+    '(context-coloring-level-1-face ((t (:foreground "#dddddd")))))
+   (enable-theme theme)
+   (context-coloring-test-assert-no-message "*Warnings*")
+   (context-coloring-test-assert-face 0 "#cccccc")
+   (context-coloring-test-assert-face 1 "#dddddd"))
+ (context-coloring-test-deftest-define-theme post-recede
+   (context-coloring-test-deftheme theme)
+   (custom-theme-set-faces
+    theme
+    '(context-coloring-level-0-face ((t (:foreground "#aaaaaa"))))
+    '(context-coloring-level-1-face ((t (:foreground "#bbbbbb")))))
+   (context-coloring-define-theme
+    theme
+    :recede t
+    :colors '("#cccccc"
+              "#dddddd"))
+   (context-coloring-test-assert-no-message "*Warnings*")
+   (context-coloring-test-assert-face 0 "#aaaaaa")
+   (context-coloring-test-assert-face 1 "#bbbbbb")
+   (enable-theme theme)
+   (context-coloring-test-assert-no-message "*Warnings*")
+   (context-coloring-test-assert-face 0 "#aaaaaa")
+   (context-coloring-test-assert-face 1 "#bbbbbb"))
+ (context-coloring-test-deftest-define-theme recede-not-defined
+   (context-coloring-test-deftheme theme)
+   (custom-theme-set-faces
+    theme
+    '(foo-face ((t (:foreground "#ffffff")))))
+   (context-coloring-define-theme
+    theme
+    :recede t
+    :colors '("#aaaaaa"
+              "#bbbbbb"))
+   (context-coloring-test-assert-no-message "*Warnings*")
+   (context-coloring-test-assert-face 0 "#aaaaaa")
+   (context-coloring-test-assert-face 1 "#bbbbbb")
+   (enable-theme theme)
+   (context-coloring-test-assert-no-message "*Warnings*")
+   (context-coloring-test-assert-face 0 "#aaaaaa")
+   (context-coloring-test-assert-face 1 "#bbbbbb"))
+ (context-coloring-test-deftest-define-theme unintentional-obstinance
+   (context-coloring-define-theme
+    theme
+    :colors '("#aaaaaa"
+              "#bbbbbb"))
+   (context-coloring-test-deftheme theme)
+   (custom-theme-set-faces
+    theme
+    '(context-coloring-level-0-face ((t (:foreground "#cccccc"))))
+    '(context-coloring-level-1-face ((t (:foreground "#dddddd")))))
+   (enable-theme theme)
+   (context-coloring-test-assert-defined-warning theme)
+   (context-coloring-test-assert-face 0 "#aaaaaa")
+   (context-coloring-test-assert-face 1 "#bbbbbb"))
+ (context-coloring-test-deftest-define-theme intentional-obstinance
+   (context-coloring-define-theme
+    theme
+    :override t
+    :colors '("#aaaaaa"
+              "#bbbbbb"))
+   (context-coloring-test-deftheme theme)
+   (custom-theme-set-faces
+    theme
+    '(context-coloring-level-0-face ((t (:foreground "#cccccc"))))
+    '(context-coloring-level-1-face ((t (:foreground "#dddddd")))))
+   (enable-theme theme)
+   (context-coloring-test-assert-no-message "*Warnings*")
+   (context-coloring-test-assert-face 0 "#aaaaaa")
+   (context-coloring-test-assert-face 1 "#bbbbbb"))
+ (defun context-coloring-test-assert-face-count (count &optional negate)
+   "Assert that `context-coloring-face-count' is COUNT."
+   (when (funcall (if negate 'identity 'not)
+                  (eq context-coloring-face-count count))
+     (ert-fail (format (concat "Expected `context-coloring-face-count' "
+                               "%sto be `%s', "
+                               "but it %s.")
+                       (if negate "not " "") count
+                       (if negate
+                           "was"
+                         (format "was `%s'" context-coloring-face-count))))))
+ (defun context-coloring-test-assert-not-face-count (&rest arguments)
+   "Assert that `context-coloring-face-count' is not COUNT."
+   (apply 'context-coloring-test-assert-face-count
+          (append arguments '(t))))
+ (context-coloring-test-deftest-define-theme disable-cascade
+   (context-coloring-test-deftheme theme)
+   (context-coloring-define-theme
+    theme
+    :colors '("#aaaaaa"
+              "#bbbbbb"))
+   (let ((second-theme (context-coloring-test-get-next-theme)))
+     (context-coloring-test-deftheme second-theme)
+     (context-coloring-define-theme
+      second-theme
+      :colors '("#cccccc"
+                "#dddddd"
+                "#eeeeee"))
+     (let ((third-theme (context-coloring-test-get-next-theme)))
+       (context-coloring-test-deftheme third-theme)
+       (context-coloring-define-theme
+        third-theme
+        :colors '("#111111"
+                  "#222222"
+                  "#333333"
+                  "#444444"))
+       (enable-theme theme)
+       (enable-theme second-theme)
+       (enable-theme third-theme)
+       (disable-theme third-theme)
+       (context-coloring-test-assert-face 0 "#cccccc")
+       (context-coloring-test-assert-face 1 "#dddddd")
+       (context-coloring-test-assert-face 2 "#eeeeee")
+       (context-coloring-test-assert-face-count 3))
+     (disable-theme second-theme)
+     (context-coloring-test-assert-face 0 "#aaaaaa")
+     (context-coloring-test-assert-face 1 "#bbbbbb")
+     (context-coloring-test-assert-face-count 2))
+   (disable-theme theme)
+   (context-coloring-test-assert-not-face 0 "#aaaaaa")
+   (context-coloring-test-assert-not-face 1 "#bbbbbb")
+   (context-coloring-test-assert-not-face-count 2))
  (defun context-coloring-test-js-function-scopes ()
    (context-coloring-test-assert-region-level 1 9 0)
    (context-coloring-test-assert-region-level 9 23 1)