]> code.delx.au - gnu-emacs-elpa/blobdiff - context-coloring.el
Don't color function calls as level 0.
[gnu-emacs-elpa] / context-coloring.el
index 853a8e85553f0bd3292f3aafffe7f730cdff7446..0f65b571fe32e5be1272ce9b791033dc997a54b7 100644 (file)
@@ -1,12 +1,12 @@
-;;; context-coloring.el --- Syntax highlighting, except not for syntax. -*- lexical-binding: t; -*-
+;;; context-coloring.el --- Highlight by scope  -*- lexical-binding: t; -*-
 
 ;; Copyright (C) 2014-2015  Free Software Foundation, Inc.
 
 ;; Author: Jackson Ray Hamilton <jackson@jacksonrayhamilton.com>
-;; URL: https://github.com/jacksonrayhamilton/context-coloring
+;; Version: 6.2.1
 ;; Keywords: convenience faces tools
-;; Version: 6.2.0
 ;; Package-Requires: ((emacs "24") (js2-mode "20150126"))
+;; URL: https://github.com/jacksonrayhamilton/context-coloring
 
 ;; This file is part of GNU Emacs.
 
 
 ;;; Commentary:
 
-;; Highlights code according to function context.
-
-;; - Code in the global scope is one color.  Code in functions within the global
-;;   scope is a different color, and code within such functions is another
-;;   color, and so on.
-;; - Identifiers retain the color of the scope in which they are declared.
+;; Highlights code by scope.  Top-level scopes are one color, second-level
+;; scopes are another color, and so on.  Variables retain the color of the scope
+;; in which they are defined.  A variable defined in an outer scope referenced
+;; by an inner scope is colored the same as the outer scope.
 
-;; Lexical scope information at-a-glance can assist a programmer in
-;; understanding the overall structure of a program.  It can help to curb nasty
-;; bugs like name shadowing.  A rainbow can indicate excessive complexity.
-;; State change within a closure is easily monitored.
+;; By default, comments and strings are still highlighted syntactically.
 
-;; By default, Context Coloring still highlights comments and strings
-;; syntactically.  It is still easy to differentiate code from non-code, and
-;; strings cannot be confused for variables.
-
-;; To use, add the following to your ~/.emacs:
+;; To use with js2-mode, add the following to your init file:
 
 ;; (require 'context-coloring)
 ;; (add-hook 'js2-mode-hook 'context-coloring-mode)
 
-;; js-mode or js3-mode support requires Node.js 0.10+ and the scopifier
-;; executable.
+;; To use with js-mode or js3-mode, install Node.js 0.10+ and the scopifier
+;; executable:
 
 ;; $ npm install -g scopifier
 
   "Join a list of STRINGS with the string DELIMITER."
   (mapconcat 'identity strings delimiter))
 
+(defsubst context-coloring-trim-right (string)
+  "Remove leading whitespace from STRING."
+  (if (string-match "[ \t\n\r]+\\'" string)
+      (replace-match "" t t string)
+    string))
+
+(defsubst context-coloring-trim-left (string)
+  "Remove trailing whitespace from STRING."
+  (if (string-match "\\`[ \t\n\r]+" string)
+      (replace-match "" t t string)
+    string))
+
+(defsubst context-coloring-trim (string)
+  "Remove leading and trailing whitespace from STRING."
+  (context-coloring-trim-left (context-coloring-trim-right string)))
+
 
 ;;; Faces
 
@@ -237,12 +244,20 @@ variable."
                        ;; `js2-prop-get-node', so this always works.
                        (eq node (js2-prop-get-node-right parent))))))))
 
+(defvar-local context-coloring-point-max nil
+  "Cached value of `point-max'.")
+
 (defsubst context-coloring-js2-colorize-node (node level)
   "Color NODE with the color for LEVEL."
   (let ((start (js2-node-abs-pos node)))
     (context-coloring-colorize-region
      start
-     (+ start (js2-node-len node)) ; End
+     (min
+      ;; End
+      (+ start (js2-node-len node))
+      ;; Somes nodes (like the ast when there is an unterminated multiline
+      ;; comment) will stretch to the value of `point-max'.
+      context-coloring-point-max)
      level)))
 
 (defun context-coloring-js2-colorize ()
@@ -250,6 +265,7 @@ variable."
 generated by `js2-mode'."
   ;; Reset the hash table; the old one could be obsolete.
   (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test 'eq))
+  (setq context-coloring-point-max (point-max))
   (with-silent-modifications
     (js2-visit-ast
      js2-mode-ast
@@ -278,6 +294,309 @@ generated by `js2-mode'."
     (context-coloring-maybe-colorize-comments-and-strings)))
 
 
+;;; Emacs Lisp colorization
+
+(defun context-coloring-make-scope (depth level)
+  (list
+   :depth depth
+   :level level
+   :variables (make-hash-table)))
+
+(defun context-coloring-scope-get-depth (scope)
+  (plist-get scope :depth))
+
+(defun context-coloring-scope-get-level (scope)
+  (plist-get scope :level))
+
+(defun context-coloring-scope-add-variable (scope variable)
+  (puthash variable t (plist-get scope :variables)))
+
+(defun context-coloring-scope-get-variable (scope variable)
+  (gethash variable (plist-get scope :variables)))
+
+(defun context-coloring-get-variable-level (scope-stack variable)
+  (let* (scope
+         level)
+    (while (and scope-stack (not level))
+      (setq scope (car scope-stack))
+      (cond
+       ((context-coloring-scope-get-variable scope variable)
+        (setq level (context-coloring-scope-get-level scope)))
+       (t
+        (setq scope-stack (cdr scope-stack)))))
+    ;; Assume global
+    (or level 0)))
+
+(defun context-coloring-make-backtick (end enabled)
+  (list
+   :end end
+   :enabled enabled))
+
+(defun context-coloring-backtick-get-end (backtick)
+  (plist-get backtick :end))
+
+(defun context-coloring-backtick-get-enabled (backtick)
+  (plist-get backtick :enabled))
+
+(defun context-coloring-backtick-enabled-p (backtick-stack)
+  (context-coloring-backtick-get-enabled (car backtick-stack)))
+
+(defun context-coloring-make-let-value (end)
+  (list
+   :end end))
+
+(defun context-coloring-let-value-get-end (let-value)
+  (plist-get let-value :end))
+
+(defun context-coloring-emacs-lisp-identifier-syntax-p (syntax-code)
+  (or (= 2 syntax-code)
+      (= 3 syntax-code)))
+
+(defun context-coloring-forward-sws ()
+  "Move forward through whitespace and comments."
+  (while (forward-comment 1)))
+
+(defun context-coloring-at-open-parenthesis ()
+  (= 4 (logand #xFFFF (car (syntax-after (point))))))
+
+(defun context-coloring-emacs-lisp-colorize ()
+  "Color the current buffer by parsing emacs lisp sexps."
+  (with-silent-modifications
+    (save-excursion
+      ;; TODO: Can probably make this lazy to the nearest defun
+      (goto-char (point-min))
+      (let* ((inhibit-point-motion-hooks t)
+             (end (point-max))
+             (last-ppss-pos (point))
+             (ppss (syntax-ppss))
+             (scope-stack `(,(context-coloring-make-scope -1 0))) ; -1 never matches a depth
+             (backtick-stack `(,(context-coloring-make-backtick -1 nil)))
+             (let-value-stack `(,(context-coloring-make-let-value -1)))
+             one-word-found-p
+             in-defun-p
+             in-lambda-p
+             in-let*-p
+             defun-arglist
+             defun-arg
+             let-varlist
+             let-var
+             variable
+             variable-end
+             variable-string
+             variable-scope-level
+             token-pos
+             token-syntax
+             token-syntax-code
+             token-char
+             child-0-pos
+             child-0-end
+             child-0-syntax
+             child-0-syntax-code
+             child-0-string
+             child-1-pos
+             child-1-end
+             child-1-syntax
+             child-1-syntax-code
+             child-2-end)
+        (while (> end (progn (skip-syntax-forward "^()w_'" end)
+                             (point)))
+          (setq token-pos (point))
+          (setq token-syntax (syntax-after token-pos))
+          (setq ppss (parse-partial-sexp last-ppss-pos token-pos nil nil ppss))
+          (setq last-ppss-pos token-pos)
+          ;; `skip-syntax-forward' leaves the point at the delimiter, move past
+          ;; it.
+          (setq token-syntax-code (logand #xFFFF (car token-syntax)))
+          (setq token-char (string-to-char (buffer-substring-no-properties
+                                            token-pos
+                                            (1+ token-pos))))
+          (cond
+
+           ;; Resolve invalid state
+           ((cond
+             ;; Inside string?
+             ((nth 3 ppss)
+              (skip-syntax-forward "^\"" end)
+              (forward-char)
+              t)
+             ;; Inside comment?
+             ((nth 4 ppss)
+              (skip-syntax-forward "^>" end) ; comment ender
+              t)))
+
+           ;; Expression prefix
+           ;; Has to come first in case of commas
+           ((= 6 token-syntax-code)
+            (forward-char)
+            (cond
+             ;; Just outright skip top-level symbols
+             ((not (or (cadr backtick-stack)
+                       (= token-char 96))) ; 96 = '`'
+              (goto-char (scan-sexps (point) 1)))
+             ((or (= token-char 96)  ; 96 = '`'
+                  (= token-char 44)) ; 44 = ','
+              ;; Have to manage backticks
+              (setq backtick-stack (cons (context-coloring-make-backtick
+                                          (scan-sexps (point) 1) ; End of the backtick
+                                          (= token-char 96)) ; 96 = '`'
+                                         backtick-stack)))))
+
+           ;; End backtick
+           ((and (cadr backtick-stack)
+                 (>= (point) (context-coloring-backtick-get-end (car backtick-stack))))
+            (setq backtick-stack (cdr backtick-stack)))
+
+           ;; Restricted by backtick
+           ((and (cadr backtick-stack)
+                 (context-coloring-backtick-enabled-p backtick-stack))
+            (forward-char))
+
+           ;; Opening delimiter
+           ((= 4 token-syntax-code)
+            (forward-char)
+            ;; Lookahead for scopes / function calls
+            (context-coloring-forward-sws)
+            (setq child-0-pos (point))
+            (setq child-0-syntax (syntax-after child-0-pos))
+            (setq child-0-syntax-code (logand #xFFFF (car child-0-syntax)))
+            (cond
+             ;; Word
+             ((context-coloring-emacs-lisp-identifier-syntax-p child-0-syntax-code)
+              (setq one-word-found-p t)
+              (setq child-0-end (scan-sexps child-0-pos 1))
+              (setq child-0-string (buffer-substring-no-properties child-0-pos child-0-end))
+              (cond
+               ((string-match-p "\\`defun\\'\\|\\`defmacro\\'" child-0-string)
+                (setq in-defun-p t))
+               ((string-match-p "\\`lambda\\'" child-0-string)
+                (setq in-lambda-p t))
+               ((string-match-p "\\`let\\*\\'" child-0-string)
+                (setq in-let*-p t)))))
+            (when (or in-defun-p
+                      in-lambda-p
+                      in-let*-p)
+              (setq scope-stack (cons (context-coloring-make-scope
+                                       (nth 0 ppss)
+                                       (1+ (context-coloring-scope-get-level
+                                            (car scope-stack))))
+                                      scope-stack)))
+            ;; TODO: Probably redundant and wasteful
+            (context-coloring-colorize-region token-pos
+                                              (scan-sexps token-pos 1)
+                                              (context-coloring-scope-get-level
+                                               (car scope-stack)))
+            (cond
+             ((or in-defun-p
+                  in-lambda-p)
+              (goto-char child-0-end)
+              (when in-defun-p
+                ;; Lookahead for defun name
+                (context-coloring-forward-sws)
+                (setq child-1-pos (point))
+                (setq child-1-syntax (syntax-after child-1-pos))
+                (setq child-1-syntax-code (logand #xFFFF (car child-1-syntax)))
+                (cond
+                 ;; Word
+                 ((context-coloring-emacs-lisp-identifier-syntax-p child-1-syntax-code)
+                  (setq child-1-end (scan-sexps child-1-pos 1))
+                  ;; defuns are global so use level 0
+                  (context-coloring-colorize-region child-1-pos child-1-end 0)
+                  (goto-char child-1-end))))
+              ;; Lookahead for parameters
+              (context-coloring-forward-sws)
+              (when (context-coloring-at-open-parenthesis)
+                ;; Actually it should be `child-1-end' for `lambda'.
+                (setq child-2-end (scan-sexps (point) 1))
+                (setq defun-arglist (read (buffer-substring-no-properties
+                                           (point)
+                                           child-2-end)))
+                (while defun-arglist
+                  (setq defun-arg (car defun-arglist))
+                  (when (and (symbolp defun-arg)
+                             (string-match-p "\\`[^&:]" (symbol-name defun-arg)))
+                    (context-coloring-scope-add-variable
+                     (car scope-stack)
+                     defun-arg))
+                  (setq defun-arglist (cdr defun-arglist)))
+                (goto-char child-2-end))
+              ;; Cleanup
+              (setq in-defun-p nil)
+              (setq in-lambda-p nil))
+             (in-let*-p
+              (goto-char child-0-end)
+              ;; Lookahead for bindings
+              (context-coloring-forward-sws)
+              (setq child-1-pos (point))
+              (setq child-1-syntax (syntax-after child-1-pos))
+              (setq child-1-syntax-code (logand #xFFFF (car child-1-syntax)))
+              (when (= 4 child-1-syntax-code)
+                (setq child-1-end (scan-sexps (point) 1))
+                (setq let-varlist (read (buffer-substring-no-properties
+                                         (point)
+                                         child-1-end)))
+                (while let-varlist
+                  (setq let-var (car let-varlist))
+                  (cond
+                   ((symbolp let-var)
+                    (context-coloring-scope-add-variable
+                     (car scope-stack)
+                     let-var))
+                   ((listp let-var)
+                    (context-coloring-scope-add-variable
+                     (car scope-stack)
+                     (car let-var))
+                    ;; TODO: Recurse or use stack to eval var value
+                    ))
+                  (setq let-varlist (cdr let-varlist)))
+                (goto-char child-1-end))
+              ;; Cleanup
+              (setq in-let*-p nil))
+             (t
+              (goto-char (cond
+                          ;; If there was a word, continue parsing after it.
+                          (one-word-found-p
+                           (1+ child-0-end))
+                          (t
+                           (1+ token-pos))))))
+            ;; Cleanup
+            (setq one-word-found-p nil))
+
+           ;; Word (variable)
+           ((context-coloring-emacs-lisp-identifier-syntax-p token-syntax-code)
+            (setq variable-end (scan-sexps (point) 1))
+            (setq variable-string (buffer-substring-no-properties
+                                   token-pos
+                                   variable-end))
+            (cond
+             ;; Ignore constants such as numbers, keywords, t, nil. These can't
+             ;; be rebound, so they should be treated like syntax.
+             ((string-match-p "\\`[-+]?[0-9]\\|\\`t\\'\\|\\`nil\\'" variable-string))
+             ((keywordp (read variable-string)))
+             (t
+              (setq variable (intern variable-string))
+              (setq variable-scope-level
+                    (context-coloring-get-variable-level scope-stack variable))
+              (when (/= variable-scope-level (context-coloring-scope-get-level
+                                              (car scope-stack)))
+                (context-coloring-colorize-region
+                 token-pos
+                 variable-end
+                 variable-scope-level))))
+            (goto-char variable-end))
+
+           ;; Closing delimiter
+           ((= 5 token-syntax-code)
+            (forward-char)
+            ;; End scope
+            (setq ppss (parse-partial-sexp last-ppss-pos (point) nil nil ppss))
+            (setq last-ppss-pos (point))
+            (when (= (nth 0 ppss) (context-coloring-scope-get-depth (car scope-stack)))
+              (setq scope-stack (cdr scope-stack))))
+
+           ))))
+    (context-coloring-maybe-colorize-comments-and-strings)))
+
+
 ;;; Shell command scopification / colorization
 
 (defun context-coloring-apply-tokens (tokens)
@@ -298,8 +617,13 @@ element."
 
 (defun context-coloring-parse-array (array)
   "Parse ARRAY as a flat JSON array of numbers."
-  (vconcat
-   (mapcar 'string-to-number (split-string (substring array 1 -1) ","))))
+  (let ((braceless (substring (context-coloring-trim array) 1 -1)))
+    (cond
+     ((> (length braceless) 0)
+      (vconcat
+       (mapcar 'string-to-number (split-string braceless ","))))
+     (t
+      (vector)))))
 
 (defvar-local context-coloring-scopifier-process nil
   "The single scopifier process that can be running.")
@@ -447,6 +771,11 @@ should be numeric, e.g. \"2\", \"19700101\", \"1.2.3\",
  (lambda ()
    (remove-hook 'js2-post-parse-callbacks 'context-coloring-colorize t)))
 
+(context-coloring-define-dispatch
+ 'emacs-lisp
+ :modes '(emacs-lisp-mode)
+ :colorizer 'context-coloring-emacs-lisp-colorize)
+
 (defun context-coloring-dispatch (&optional callback)
   "Determine the optimal track for scopification / coloring of
 the current buffer, then execute it.
@@ -645,7 +974,9 @@ which must already exist and which *should* already be enabled."
   (let* ((properties (gethash theme context-coloring-theme-hash-table))
          (colors (plist-get properties :colors))
          (level -1))
-    (setq context-coloring-maximum-face (- (length colors) 1))
+    ;; Only clobber when we have to.
+    (when (custom-theme-enabled-p theme)
+      (setq context-coloring-maximum-face (- (length colors) 1)))
     (apply
      'custom-theme-set-faces
      theme
@@ -743,7 +1074,7 @@ precedence, i.e. the car of `custom-enabled-themes'."
   (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))
-    (when (= (length custom-enabled-themes) 0)
+    (when (= (length custom-enabled-themes) 1)
       ;; Cache because we can't reliably figure it out in reverse.
       (setq context-coloring-original-maximum-face
             context-coloring-maximum-face))
@@ -770,7 +1101,7 @@ precedence, i.e. the car of `custom-enabled-themes'."
            "#5180b3"
            "#ab75c3"
            "#cd7542"
-           "#dF9522"
+           "#df9522"
            "#454545"))
 
 (context-coloring-define-theme
@@ -804,27 +1135,27 @@ precedence, i.e. the car of `custom-enabled-themes'."
  'leuven
  :recede t
  :colors '("#333333"
-           "#0000FF"
-           "#6434A3"
-           "#BA36A5"
-           "#D0372D"
-           "#036A07"
+           "#0000ff"
+           "#6434a3"
+           "#ba36a5"
+           "#d0372d"
+           "#036a07"
            "#006699"
-           "#006FE0"
+           "#006fe0"
            "#808080"))
 
 (context-coloring-define-theme
  'monokai
  :recede t
- :colors '("#F8F8F2"
-           "#66D9EF"
-           "#A1EFE4"
-           "#A6E22E"
-           "#E6DB74"
-           "#FD971F"
-           "#F92672"
-           "#FD5FF0"
-           "#AE81FF"))
+ :colors '("#f8f8f2"
+           "#66d9ef"
+           "#a1efe4"
+           "#a6e22e"
+           "#e6db74"
+           "#fd971f"
+           "#f92672"
+           "#fd5ff0"
+           "#ae81ff"))
 
 (context-coloring-define-theme
  'solarized
@@ -842,26 +1173,26 @@ precedence, i.e. the car of `custom-enabled-themes'."
            "#dc322f"
            "#d33682"
            "#6c71c4"
-           "#69B7F0"
-           "#69CABF"
-           "#B4C342"
-           "#DEB542"
-           "#F2804F"
-           "#FF6E64"
-           "#F771AC"
-           "#9EA0E5"))
+           "#69b7f0"
+           "#69cabf"
+           "#b4c342"
+           "#deb542"
+           "#f2804f"
+           "#ff6e64"
+           "#f771ac"
+           "#9ea0e5"))
 
 (context-coloring-define-theme
  'spacegray
  :recede t
  :colors '("#ffffff"
-           "#89AAEB"
-           "#C189EB"
+           "#89aaeb"
+           "#c189eb"
            "#bf616a"
-           "#DCA432"
+           "#dca432"
            "#ebcb8b"
-           "#B4EB89"
-           "#89EBCA"))
+           "#b4eb89"
+           "#89ebca"))
 
 (context-coloring-define-theme
  'tango
@@ -883,17 +1214,17 @@ precedence, i.e. the car of `custom-enabled-themes'."
 (context-coloring-define-theme
  'zenburn
  :recede t
- :colors '("#DCDCCC"
-           "#93E0E3"
-           "#BFEBBF"
-           "#F0DFAF"
-           "#DFAF8F"
-           "#CC9393"
-           "#DC8CC3"
-           "#94BFF3"
-           "#9FC59F"
-           "#D0BF8F"
-           "#DCA3A3"))
+ :colors '("#dcdccc"
+           "#93e0e3"
+           "#bfebbf"
+           "#f0dfaf"
+           "#dfaf8f"
+           "#cc9393"
+           "#dc8cc3"
+           "#94bff3"
+           "#9fc59f"
+           "#d0bf8f"
+           "#dca3a3"))
 
 
 ;;; Minor mode