(require 'js2-mode)
-;;; Local variables
-
-(defvar-local context-coloring-buffer nil
- "Reference to this buffer (for timers).")
-
-
;;; Utilities
(defun context-coloring-join (strings delimiter)
"Tell `font-lock' to color a string but not a comment."
(if (nth 3 state) font-lock-string-face nil))
-(defsubst context-coloring-maybe-colorize-comments-and-strings ()
+(defsubst context-coloring-maybe-colorize-comments-and-strings (&optional min max)
"Color the current buffer's comments and strings if
`context-coloring-comments-and-strings' is non-nil."
(when (or context-coloring-comments-and-strings
context-coloring-syntactic-comments
context-coloring-syntactic-strings)
- (let ((old-function font-lock-syntactic-face-function)
- saved-function-p)
- (cond
- ((and context-coloring-syntactic-comments
- (not context-coloring-syntactic-strings))
- (setq font-lock-syntactic-face-function
- 'context-coloring-font-lock-syntactic-comment-function)
- (setq saved-function-p t))
- ((and context-coloring-syntactic-strings
- (not context-coloring-syntactic-comments))
- (setq font-lock-syntactic-face-function
- 'context-coloring-font-lock-syntactic-string-function)
- (setq saved-function-p t)))
+ (let ((min (or min (point-min)))
+ (max (or max (point-max)))
+ (font-lock-syntactic-face-function
+ (cond
+ ((and context-coloring-syntactic-comments
+ (not context-coloring-syntactic-strings))
+ 'context-coloring-font-lock-syntactic-comment-function)
+ ((and context-coloring-syntactic-strings
+ (not context-coloring-syntactic-comments))
+ 'context-coloring-font-lock-syntactic-string-function)
+ (t
+ font-lock-syntactic-face-function))))
(save-excursion
- (font-lock-fontify-syntactically-region (point-min) (point-max)))
- (when saved-function-p
- (setq font-lock-syntactic-face-function old-function)))))
+ (font-lock-fontify-syntactically-region min max)
+ ;; TODO: Make configurable at the dispatch level.
+ (when (eq major-mode 'emacs-lisp-mode)
+ (font-lock-fontify-keywords-region min max))))))
;;; js2-mode colorization
(defun context-coloring-stack-depth-equal (stack depth)
(= (plist-get (car stack) :depth) depth))
+(defun context-coloring-exact-regexp (word)
+ "Create a regexp that matches exactly WORD."
+ (concat "\\`" (regexp-quote word) "\\'"))
+
+(defun context-coloring-exact-or-regexp (words)
+ "Create a regexp that matches any exact word in WORDS."
+ (context-coloring-join
+ (mapcar 'context-coloring-exact-regexp words) "\\|"))
+
(defconst context-coloring-defun-regexp
- "\\`defun\\'\\|\\`defmacro\\'\\|\\`defsubst\\'")
+ (context-coloring-exact-or-regexp
+ '("defun" "defun*" "defsubst" "defmacro"
+ "cl-defun" "cl-defsubst" "cl-defmacro")))
(defconst context-coloring-arglist-arg-regexp
"\\`[^&:]")
(defconst context-coloring-ignored-word-regexp
- "\\`[-+]?[0-9]\\|\\`t\\'\\|\\`nil\\'\\|\\`\\.\\'")
+ (concat "\\`[-+]?[0-9]\\|" (context-coloring-exact-or-regexp
+ '("t" "nil" "." "?"))))
(defconst context-coloring-COMMA-CHAR 44)
(defconst context-coloring-BACKTICK-CHAR 96)
+(defvar context-coloring-parse-interruptable-p t
+ "Set this to nil to force parse to continue until finished.")
+
+(defvar context-coloring-emacs-lisp-iterations-per-pause 1000
+ "Pause after this many iterations to check for user input.
+If user input is pending, stop the parse. This makes for a
+smoother user experience for large files.
+
+As of this writing, emacs lisp colorization seems to run at about
+60,000 iterations per second. A default value of 1000 should
+provide visually \"instant\" updates at ~60 frames per second.")
+
(defun context-coloring-emacs-lisp-colorize ()
"Color the current buffer by parsing emacs lisp sexps."
(with-silent-modifications
;; TODO: Can probably make this lazy to the nearest defun.
(goto-char (point-min))
(let* ((inhibit-point-motion-hooks t)
+ (iteration-count 0)
+ (last-fontified-position (point))
+ beginning-of-current-defun
+ end-of-current-defun
(end (point-max))
(last-ppss-pos (point))
(ppss (syntax-ppss))
child-2-end)
(while (> end (progn (skip-syntax-forward "^()w_'" end)
(point)))
+ ;; Sparingly-executed tasks.
+ (setq iteration-count (1+ iteration-count))
+ (when (zerop (% iteration-count
+ context-coloring-emacs-lisp-iterations-per-pause))
+ ;; Fontify until the end of the current defun because doing it in
+ ;; chunks based soley on point could result in partial
+ ;; re-fontifications over the contents of scopes.
+ (save-excursion
+ (end-of-defun)
+ (setq end-of-current-defun (point))
+ (beginning-of-defun)
+ (setq beginning-of-current-defun (point)))
+
+ ;; Fontify in chunks.
+ (context-coloring-maybe-colorize-comments-and-strings
+ last-fontified-position
+ (cond
+ ;; We weren't actually in a defun, so don't color the next one, as
+ ;; that could result in `font-lock' properties being added to it.
+ ((> beginning-of-current-defun (point))
+ (point))
+ (t
+ end-of-current-defun)))
+ (setq last-fontified-position (point))
+ (when (and context-coloring-parse-interruptable-p
+ (input-pending-p))
+ (throw 'interrupted t)))
+
(setq token-pos (point))
(setq token-syntax (syntax-after token-pos))
(setq token-syntax-code (logand #xFFFF (car token-syntax)))
(context-coloring-scope-add-variable (car scope-stack) (car popped-vars))
(setq popped-vars (cdr popped-vars))))
- ))))
- (context-coloring-maybe-colorize-comments-and-strings)))
+ ))
+ ;; Fontify the last stretch.
+ (context-coloring-maybe-colorize-comments-and-strings
+ last-fontified-position
+ (point))))))
;;; Shell command scopification / colorization
list of tokens to `context-coloring-apply-tokens'.
Invoke CALLBACK when complete."
- (let ((buffer context-coloring-buffer))
+ (let ((buffer (current-buffer)))
(context-coloring-scopify-shell-command
command
(lambda (output)
(error "No colorizer, scopifier or command defined for dispatch"))
(puthash symbol properties context-coloring-dispatch-hash-table)
(dolist (mode modes)
- (when (null (gethash mode context-coloring-mode-hash-table))
- (puthash mode properties context-coloring-mode-hash-table)))))
+ (puthash mode properties context-coloring-mode-hash-table))))
;;; Colorization
(context-coloring-kill-scopifier)
(setq context-coloring-changed t))
-(defun context-coloring-maybe-colorize ()
+(defun context-coloring-maybe-colorize (buffer)
"Colorize the current buffer if it has changed."
- (when (and (eq context-coloring-buffer (window-buffer (selected-window)))
+ (when (and (eq buffer (current-buffer))
context-coloring-changed)
(setq context-coloring-changed nil)
(context-coloring-colorize)))
"Setup idle change detection."
(add-hook
'after-change-functions 'context-coloring-change-function nil t)
+ (add-hook
+ 'kill-buffer-hook 'context-coloring-teardown-idle-change-detection nil t)
(setq context-coloring-colorize-idle-timer
(run-with-idle-timer
context-coloring-delay
t
- 'context-coloring-maybe-colorize)))
+ 'context-coloring-maybe-colorize
+ (current-buffer))))
+
+(defun context-coloring-teardown-idle-change-detection ()
+ "Teardown idle change detection."
+ (context-coloring-kill-scopifier)
+ (when context-coloring-colorize-idle-timer
+ (cancel-timer context-coloring-colorize-idle-timer))
+ (remove-hook
+ 'kill-buffer-hook 'context-coloring-teardown-idle-change-detection t)
+ (remove-hook
+ 'after-change-functions 'context-coloring-change-function t))
;;; Built-in dispatches
'emacs-lisp
:modes '(emacs-lisp-mode)
:colorizer 'context-coloring-emacs-lisp-colorize
- :setup
- (lambda ()
- (context-coloring-setup-idle-change-detection))
- :teardown
- (lambda ()
- (when context-coloring-colorize-idle-timer
- (cancel-timer context-coloring-colorize-idle-timer))
- (remove-hook
- 'after-change-functions 'context-coloring-change-function t)))
+ :setup 'context-coloring-setup-idle-change-detection
+ :teardown 'context-coloring-teardown-idle-change-detection)
(defun context-coloring-dispatch (&optional callback)
"Determine the optimal track for scopification / coloring of
Invoke CALLBACK when complete. It is invoked synchronously for
elisp tracks, and asynchronously for shell command tracks."
- (let ((dispatch (gethash major-mode context-coloring-mode-hash-table))
- colorizer
- scopifier
- command)
+ (let* ((dispatch (gethash major-mode context-coloring-mode-hash-table))
+ (colorizer (plist-get dispatch :colorizer))
+ (scopifier (plist-get dispatch :scopifier))
+ (command (plist-get dispatch :command))
+ interrupted-p)
(cond
- ((setq colorizer (plist-get dispatch :colorizer))
- (funcall colorizer)
- (when callback (funcall callback)))
- ((setq scopifier (plist-get dispatch :scopifier))
- (context-coloring-apply-tokens (funcall scopifier))
- (when callback (funcall callback)))
- ((setq command (plist-get dispatch :command))
+ ((or colorizer scopifier)
+ (setq interrupted-p
+ (catch 'interrupted
+ (cond
+ (colorizer
+ (funcall colorizer))
+ (scopifier
+ (context-coloring-apply-tokens (funcall scopifier))))))
+ (cond
+ (interrupted-p
+ (setq context-coloring-changed t))
+ (t
+ (when callback (funcall callback)))))
+ (command
(context-coloring-scopify-and-colorize command callback)))))
nil " Context" nil
(if (not context-coloring-mode)
(progn
- (context-coloring-kill-scopifier)
- (when context-coloring-colorize-idle-timer
- (cancel-timer context-coloring-colorize-idle-timer))
(let ((dispatch (gethash major-mode context-coloring-mode-hash-table)))
(when dispatch
(let ((command (plist-get dispatch :command))
(teardown (plist-get dispatch :teardown)))
(when command
- (remove-hook
- 'after-change-functions 'context-coloring-change-function t))
+ (context-coloring-teardown-idle-change-detection))
(when teardown
(funcall teardown)))))
(font-lock-mode)
(jit-lock-mode t))
- ;; 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.
(font-lock-mode 0)
(jit-lock-mode nil)
+ ;; ...but we do use font-lock functions here.
+ (font-lock-set-defaults)
+
;; Safely change the valye of this function as necessary.
(make-local-variable 'font-lock-syntactic-face-function)
+ ;; TODO: Detect derived modes.
(let ((dispatch (gethash major-mode context-coloring-mode-hash-table)))
(if dispatch
(progn
(funcall setup))
;; Colorize once initially.
(when colorize-initially-p
- (context-coloring-colorize))))
+ (let ((context-coloring-parse-interruptable-p nil))
+ (context-coloring-colorize)))))
(when (null dispatch)
(message "Context coloring is not available for this major mode"))))))