branch: master commit c6e173b4d603cdc54080569bbc78038473921a97 Merge: eb429df 3b6a391 Author: Jackson Ray Hamilton <jack...@jacksonrayhamilton.com> Commit: Jackson Ray Hamilton <jack...@jacksonrayhamilton.com>
Merge branch 'elisp' --- README.md | 24 ++- context-coloring.el | 657 ++++++++++++++++++++++++++++++++++++----- test/context-coloring-test.el | 275 ++++++++++++++++- test/fixtures/comment.el | 3 + test/fixtures/defun.el | 7 + test/fixtures/ignored.el | 2 + test/fixtures/iteration.el | 2 + test/fixtures/lambda.el | 3 + test/fixtures/let*.el | 11 + test/fixtures/let.el | 8 + test/fixtures/quote.el | 4 + test/fixtures/string.el | 2 + 12 files changed, 894 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index bc21b62..39c15cf 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,12 @@ By default, comments and strings are still highlighted syntactically. ## Features -- Supported languages: JavaScript - Light and dark (customizable) color schemes. -- Very fast for files under 1000 lines. +- JavaScript support: + - Very fast for files under 1000 lines. + - Script, function and block scopes (and even `catch` block scopes). +- Emacs Lisp support: + - `defun`, `lambda`, `let`, `let*`, quotes, backticks, commas. ## Installation @@ -51,7 +54,7 @@ make compile (require 'context-coloring) ``` -### scopifier (for non-js2-mode users) +### Dependencies (js-mode) ```bash npm install -g scopifier @@ -62,12 +65,15 @@ npm install -g scopifier Add the following to your init file: ```lisp -;; non-js2-mode users: +;; js-mode: (add-hook 'js-mode-hook 'context-coloring-mode) -;; js2-mode users: +;; js2-mode: (add-to-list 'auto-mode-alist '("\\.js\\'" . js2-mode)) (add-hook 'js2-mode-hook 'context-coloring-mode) + +;; emacs-lisp-mode: +(add-hook 'emacs-lisp-mode-hook 'context-coloring-mode) ``` ## Customizing @@ -79,7 +85,8 @@ Add the following to your init file: - `context-coloring-syntactic-strings` (default: `t`): If non-nil, also color strings using `font-lock`. - `context-coloring-delay` (default: `0.25`; supported modes: `js-mode`, - `js3-mode`): Delay between a buffer update and colorization. + `js3-mode`, `emacs-lisp-mode`): Delay between a buffer update and + colorization. - `context-coloring-js-block-scopes` (default: `nil`; supported modes: `js2-mode`): If non-nil, also color block scopes in the scope hierarchy in JavaScript. @@ -170,6 +177,11 @@ When a `--version` argument is passed, a scopifier should print its version number and exit. This allows context-coloring to determine if an update is required. +Alternatively, you could implement a "colorizer" in Emacs Lisp. A colorizer +also handles the job of calling `context-coloring-colorize-region` to apply +colors to a buffer. A colorizer may have better performance than a scopifier +when parsing and coloring can be performed in the same pass. + [js2-mode]: https://github.com/mooz/js2-mode [node]: http://nodejs.org/download/ [scopifier]: https://github.com/jacksonrayhamilton/scopifier diff --git a/context-coloring.el b/context-coloring.el index c5c7d3f..5c3b76f 100644 --- a/context-coloring.el +++ b/context-coloring.el @@ -47,12 +47,6 @@ (require 'js2-mode) -;;; Local variables - -(defvar-local context-coloring-buffer nil - "Reference to this buffer (for timers).") - - ;;; Utilities (defun context-coloring-join (strings delimiter) @@ -172,29 +166,29 @@ the END point (exclusive) with the face corresponding to LEVEL." "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 @@ -294,6 +288,468 @@ generated by `js2-mode'." (context-coloring-maybe-colorize-comments-and-strings))) +;;; Emacs Lisp colorization + +(defsubst context-coloring-make-scope (depth level) + (list + :depth depth + :level level + :variables (make-hash-table))) + +(defsubst context-coloring-scope-get-level (scope) + (plist-get scope :level)) + +(defsubst context-coloring-scope-add-variable (scope variable) + (puthash variable t (plist-get scope :variables))) + +(defsubst context-coloring-scope-get-variable (scope variable) + (gethash variable (plist-get scope :variables))) + +(defsubst 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 a global variable. + (or level 0))) + +(defsubst context-coloring-make-backtick (end enabled) + (list + :end end + :enabled enabled)) + +(defsubst context-coloring-backtick-get-end (backtick) + (plist-get backtick :end)) + +(defsubst context-coloring-backtick-get-enabled (backtick) + (plist-get backtick :enabled)) + +(defsubst context-coloring-backtick-enabled-p (backtick-stack) + (context-coloring-backtick-get-enabled (car backtick-stack))) + +(defsubst context-coloring-make-let-varlist (depth type) + (list + :depth depth + :type type + :vars '())) + +(defsubst context-coloring-let-varlist-get-type (let-varlist) + (plist-get let-varlist :type)) + +(defsubst context-coloring-let-varlist-add-var (let-varlist var) + (plist-put let-varlist :vars (cons var (plist-get let-varlist :vars)))) + +(defsubst context-coloring-let-varlist-pop-vars (let-varlist) + (let ((type (context-coloring-let-varlist-get-type let-varlist)) + (vars (plist-get let-varlist :vars))) + (cond + ;; `let' binds all at once at the end. + ((eq type 'let) + (prog1 + vars + (plist-put let-varlist :vars '()))) + ;; `let*' binds incrementally. + ((eq type 'let*) + (prog1 + (list (car vars)) + (plist-put let-varlist :vars (cdr vars))))))) + +(defsubst context-coloring-forward-sws () + "Move forward through whitespace and comments." + (while (forward-comment 1))) + +(defsubst context-coloring-forward-sexp-position () + "Like vanilla `forward-sexp', but just return the position." + (scan-sexps (point) 1)) + +(defsubst context-coloring-emacs-lisp-identifier-syntax-p (syntax-code) + (or (= 2 syntax-code) + (= 3 syntax-code))) + +(defsubst context-coloring-open-parenthesis-p (syntax-code) + (= 4 syntax-code)) + +(defsubst context-coloring-close-parenthesis-p (syntax-code) + (= 5 syntax-code)) + +(defsubst context-coloring-expression-prefix-p (syntax-code) + (= 6 syntax-code)) + +(defsubst context-coloring-at-open-parenthesis-p () + (= 4 (logand #xFFFF (car (syntax-after (point)))))) + +(defsubst context-coloring-ppss-depth (ppss) + ;; Same as (nth 0 ppss). + (car ppss)) + +(defsubst context-coloring-at-stack-depth-p (stack depth) + (= (plist-get (car stack) :depth) depth)) + +(defsubst context-coloring-exact-regexp (word) + "Create a regexp that matches exactly WORD." + (concat "\\`" (regexp-quote word) "\\'")) + +(defsubst 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-emacs-lisp-defun-regexp + (context-coloring-exact-or-regexp + '("defun" "defun*" "defsubst" "defmacro" + "cl-defun" "cl-defsubst" "cl-defmacro"))) + +(defconst context-coloring-emacs-lisp-lambda-regexp + (context-coloring-exact-regexp "lambda")) + +(defconst context-coloring-emacs-lisp-let-regexp + (context-coloring-exact-regexp "let")) + +(defconst context-coloring-emacs-lisp-let*-regexp + (context-coloring-exact-regexp "let*")) + +(defconst context-coloring-arglist-arg-regexp + "\\`[^&:]") + +(defconst context-coloring-ignored-word-regexp + (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.") + +(defconst 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 + (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)) + (iteration-count 0) + (last-fontified-position (point)) + beginning-of-current-defun + end-of-current-defun + (last-ppss-pos (point)) + (ppss (syntax-ppss)) + ppss-depth + ;; -1 never matches a depth. This is a minor optimization. + (scope-stack `(,(context-coloring-make-scope -1 0))) + (backtick-stack '()) + (let-varlist-stack '()) + (let-var-stack '()) + popped-vars + one-word-found-p + in-defun-p + in-lambda-p + in-let-p + in-let*-p + defun-arglist + defun-arg + let-varlist + let-varlist-type + 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))) + ;; 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))) + (setq token-char (char-after)) + (setq ppss (parse-partial-sexp last-ppss-pos token-pos nil nil ppss)) + (setq last-ppss-pos token-pos) + (cond + + ;; Resolve an invalid state. + ((cond + ;; Inside string? + ((nth 3 ppss) + (skip-syntax-forward "^\"" end) + (forward-char) + t) + ;; Inside comment? + ((nth 4 ppss) + (skip-syntax-forward "^>" end) + t))) + + ;; Need to check early in case there's a comma. + ((context-coloring-expression-prefix-p token-syntax-code) + (forward-char) + (cond + ;; Skip top-level symbols. + ((not (or backtick-stack + (= token-char context-coloring-BACKTICK-CHAR))) + (goto-char (context-coloring-forward-sexp-position))) + ;; Push a backtick state. + ((or (= token-char context-coloring-BACKTICK-CHAR) + (= token-char context-coloring-COMMA-CHAR)) + (setq backtick-stack (cons (context-coloring-make-backtick + (context-coloring-forward-sexp-position) + (= token-char context-coloring-BACKTICK-CHAR)) + backtick-stack))))) + + ;; Pop a backtick state. + ((and backtick-stack + (>= (point) (context-coloring-backtick-get-end (car backtick-stack)))) + (setq backtick-stack (cdr backtick-stack))) + + ;; Restricted by an enabled backtick. + ((and backtick-stack + (context-coloring-backtick-enabled-p backtick-stack)) + (forward-char)) + + ((context-coloring-open-parenthesis-p token-syntax-code) + (forward-char) + ;; Look for 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 + ((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 + ;; Parse a var in a `let' varlist. + ((and + let-varlist-stack + (context-coloring-at-stack-depth-p + let-varlist-stack + ;; 1- because we're inside the varlist. + (1- (context-coloring-ppss-depth ppss)))) + (context-coloring-let-varlist-add-var + (car let-varlist-stack) + (intern child-0-string)) + (setq let-var-stack (cons (context-coloring-ppss-depth ppss) + let-var-stack))) + ((string-match-p context-coloring-emacs-lisp-defun-regexp child-0-string) + (setq in-defun-p t)) + ((string-match-p context-coloring-emacs-lisp-lambda-regexp child-0-string) + (setq in-lambda-p t)) + ((string-match-p context-coloring-emacs-lisp-let-regexp child-0-string) + (setq in-let-p t) + (setq let-varlist-type 'let)) + ((string-match-p context-coloring-emacs-lisp-let*-regexp child-0-string) + (setq in-let*-p t) + (setq let-varlist-type 'let*))))) + (when (or in-defun-p + in-lambda-p + in-let-p + in-let*-p) + (setq scope-stack (cons (context-coloring-make-scope + (context-coloring-ppss-depth ppss) + (1+ (context-coloring-scope-get-level + (car scope-stack)))) + scope-stack))) + ;; TODO: Maybe wasteful but doing this conditionally doesn't make + ;; much of a difference. + (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 + ;; Look for a function 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 + ((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)))) + ;; Look for an arglist. + (context-coloring-forward-sws) + (when (context-coloring-at-open-parenthesis-p) + ;; (Actually it should be `child-1-end' for `lambda'.) + (setq child-2-end (context-coloring-forward-sexp-position)) + (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 + context-coloring-arglist-arg-regexp + (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)) + ((or in-let-p + in-let*-p) + (goto-char child-0-end) + ;; Look for a varlist. + (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 (context-coloring-open-parenthesis-p child-1-syntax-code) + ;; Begin parsing the varlist. + (forward-char) + (setq let-varlist-stack (cons (context-coloring-make-let-varlist + ;; 1+ because we parsed it at a + ;; higher depth. + (1+ (context-coloring-ppss-depth ppss)) + let-varlist-type) + let-varlist-stack))) + ;; Cleanup. + (setq in-let-p nil) + (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)) + + ((context-coloring-emacs-lisp-identifier-syntax-p token-syntax-code) + (setq variable-end (context-coloring-forward-sexp-position)) + (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 context-coloring-ignored-word-regexp variable-string)) + ((keywordp (read variable-string))) + (t + (setq variable (intern variable-string)) + (cond + ;; Parse a `let' varlist's uninitialized var. + ((and + let-varlist-stack + (context-coloring-at-stack-depth-p + let-varlist-stack + ;; 1- because we're inside the varlist. + (1- (context-coloring-ppss-depth ppss)))) + (setq let-varlist (car let-varlist-stack)) + (setq let-varlist-type (context-coloring-let-varlist-get-type let-varlist)) + (cond + ;; Defer `let' binding until the end of the varlist. + ((eq let-varlist-type 'let) + (context-coloring-let-varlist-add-var let-varlist variable)) + ;; Bind a `let*' right away. + ((eq let-varlist-type 'let*) + (context-coloring-scope-add-variable (car scope-stack) variable)))) + (t + (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)) + + ((context-coloring-close-parenthesis-p token-syntax-code) + (forward-char) + (setq ppss (parse-partial-sexp last-ppss-pos (point) nil nil ppss)) + (setq last-ppss-pos (point)) + (setq ppss-depth (context-coloring-ppss-depth ppss)) + ;; TODO: Order might matter here but I'm not certain. + (when (context-coloring-at-stack-depth-p scope-stack ppss-depth) + (setq scope-stack (cdr scope-stack))) + (when (and + let-var-stack + (= (car let-var-stack) ppss-depth)) + (setq let-var-stack (cdr let-var-stack)) + (when (eq (context-coloring-let-varlist-get-type (car let-varlist-stack)) + 'let*) + (setq popped-vars (context-coloring-let-varlist-pop-vars + (car let-varlist-stack))))) + (when (and + let-varlist-stack + (context-coloring-at-stack-depth-p let-varlist-stack ppss-depth)) + (setq popped-vars (context-coloring-let-varlist-pop-vars + (car let-varlist-stack))) + (setq let-varlist-stack (cdr let-varlist-stack))) + (while popped-vars + (context-coloring-scope-add-variable (car scope-stack) (car popped-vars)) + (setq popped-vars (cdr popped-vars)))) + + )) + ;; Fontify the last stretch. + (context-coloring-maybe-colorize-comments-and-strings + last-fontified-position + (point)))))) + + ;;; Shell command scopification / colorization (defun context-coloring-apply-tokens (tokens) @@ -375,7 +831,7 @@ read the scopifier's response asynchronously and apply a parsed 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) @@ -396,6 +852,15 @@ Invoke CALLBACK when complete." (defvar context-coloring-mode-hash-table (make-hash-table :test 'eq) "Map major mode names to dispatch property lists.") +(defun context-coloring-get-dispatch-for-mode (mode) + "Return the dispatch for MODE (or a derivative mode)." + (let ((parent mode) + dispatch) + (while (and parent + (not (setq dispatch (gethash parent context-coloring-mode-hash-table))) + (setq parent (get parent 'derived-mode-parent)))) + dispatch)) + (defun context-coloring-define-dispatch (symbol &rest properties) "Define a new dispatch named SYMBOL with PROPERTIES. @@ -447,46 +912,7 @@ should be numeric, e.g. \"2\", \"19700101\", \"1.2.3\", (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))))) - -(context-coloring-define-dispatch - 'javascript-node - :modes '(js-mode js3-mode) - :executable "scopifier" - :command "scopifier" - :version "v1.1.1") - -(context-coloring-define-dispatch - 'javascript-js2 - :modes '(js2-mode) - :colorizer 'context-coloring-js2-colorize - :setup - (lambda () - (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t)) - :teardown - (lambda () - (remove-hook 'js2-post-parse-callbacks 'context-coloring-colorize t))) - -(defun context-coloring-dispatch (&optional callback) - "Determine the optimal track for scopification / coloring of -the current buffer, then execute it. - -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) - (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)) - (context-coloring-scopify-and-colorize command callback))))) + (puthash mode properties context-coloring-mode-hash-table)))) ;;; Colorization @@ -516,9 +942,9 @@ used.") (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))) @@ -563,7 +989,7 @@ produces (1 0 0), \"19700101\" produces (19700101), etc." "Asynchronously invoke CALLBACK with a predicate indicating whether the current scopifier version satisfies the minimum version number required for the current major mode." - (let ((dispatch (gethash major-mode context-coloring-mode-hash-table))) + (let ((dispatch (context-coloring-get-dispatch-for-mode major-mode))) (when dispatch (let ((version (plist-get dispatch :version)) (command (plist-get dispatch :command))) @@ -919,7 +1345,7 @@ precedence, i.e. the car of `custom-enabled-themes'." "#dca3a3")) -;;; Minor mode +;;; Change detection (defvar-local context-coloring-colorize-idle-timer nil "The currently-running idle timer.") @@ -930,18 +1356,90 @@ precedence, i.e. the car of `custom-enabled-themes'." Increase this if your machine is high-performing. Decrease it if it ain't. -Supported modes: `js-mode', `js3-mode'" +Supported modes: `js-mode', `js3-mode', `emacs-lisp-mode'" :group 'context-coloring) (defun context-coloring-setup-idle-change-detection () "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 + +(context-coloring-define-dispatch + 'javascript-node + :modes '(js-mode js3-mode) + :executable "scopifier" + :command "scopifier" + :version "v1.1.1") + +(context-coloring-define-dispatch + 'javascript-js2 + :modes '(js2-mode) + :colorizer 'context-coloring-js2-colorize + :setup + (lambda () + (add-hook 'js2-post-parse-callbacks 'context-coloring-colorize nil t)) + :teardown + (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 + :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 +the current buffer, then execute it. + +Invoke CALLBACK when complete. It is invoked synchronously for +elisp tracks, and asynchronously for shell command tracks." + (let* ((dispatch (context-coloring-get-dispatch-for-mode major-mode)) + (colorizer (plist-get dispatch :colorizer)) + (scopifier (plist-get dispatch :scopifier)) + (command (plist-get dispatch :command)) + interrupted-p) + (cond + ((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))))) + + +;;; Minor mode ;;;###autoload (define-minor-mode context-coloring-mode @@ -949,32 +1447,28 @@ Supported modes: `js-mode', `js3-mode'" 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))) + (let ((dispatch (context-coloring-get-dispatch-for-mode major-mode))) (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) - (let ((dispatch (gethash major-mode context-coloring-mode-hash-table))) + (let ((dispatch (context-coloring-get-dispatch-for-mode major-mode))) (if dispatch (progn (let ((command (plist-get dispatch :command)) @@ -1005,7 +1499,8 @@ Supported modes: `js-mode', `js3-mode'" (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")))))) diff --git a/test/context-coloring-test.el b/test/context-coloring-test.el index b9a43d9..e22ee29 100644 --- a/test/context-coloring-test.el +++ b/test/context-coloring-test.el @@ -44,13 +44,12 @@ (defun context-coloring-test-setup () "Prepare before all tests." - (setq context-coloring-comments-and-strings nil)) + (setq context-coloring-syntactic-comments nil) + (setq context-coloring-syntactic-strings nil)) (defun context-coloring-test-cleanup () "Cleanup after all tests." - (setq context-coloring-comments-and-strings t) - (setq context-coloring-syntactic-comments nil) - (setq context-coloring-syntactic-strings nil) + (setq context-coloring-comments-and-strings nil) (setq context-coloring-js-block-scopes nil) (setq context-coloring-colorize-hook nil) (setq context-coloring-check-scopifier-version-hook nil) @@ -167,9 +166,123 @@ format." ',setup-function-name (,function-name))))) +(cl-defmacro context-coloring-test-deftest-emacs-lisp-mode (name + body + &key setup) + "Define a test for `emacs-lisp-mode' with name and fixture as +NAME, with BODY containing the assertions, and SETUP defining the +environment." + (declare (indent defun)) + (let ((test-name (intern (format "context-coloring-emacs-lisp-mode-%s" name))) + (fixture (format "./fixtures/%s.el" name))) + `(ert-deftest ,test-name () + (context-coloring-test-with-fixture + ,fixture + (emacs-lisp-mode) + (when ,setup (funcall ,setup)) + (context-coloring-mode) + (funcall ,body))))) + ;;; Assertion functions +(defun context-coloring-test-assert-position-level (position level) + "Assert that POSITION has LEVEL." + (let ((face (get-text-property position 'face)) + actual-level) + (when (not (and face + (let* ((face-string (symbol-name face)) + (matches (string-match + context-coloring-level-face-regexp + face-string))) + (when matches + (setq actual-level (string-to-number + (substring face-string + (match-beginning 1) + (match-end 1)))) + (= level actual-level))))) + (ert-fail (format (concat "Expected level at position %s, " + "which is \"%s\", to be %s; " + "but it was %s") + position + (buffer-substring-no-properties position (1+ position)) level + actual-level))))) + +(defun context-coloring-test-assert-position-face (position face-regexp) + "Assert that the face at POSITION satisfies FACE-REGEXP." + (let ((face (get-text-property position 'face))) + (when (or + ;; Pass a non-string to do an `equal' check (against a symbol or nil). + (unless (stringp face-regexp) + (not (equal face-regexp face))) + ;; Otherwise do the matching. + (when (stringp face-regexp) + (not (string-match-p face-regexp (symbol-name face))))) + (ert-fail (format (concat "Expected face at position %s, " + "which is \"%s\", to be %s; " + "but it was %s") + position + (buffer-substring-no-properties position (1+ position)) face-regexp + face))))) + +(defun context-coloring-test-assert-position-comment (position) + (context-coloring-test-assert-position-face + position "\\`font-lock-comment\\(-delimiter\\)?-face\\'")) + +(defun context-coloring-test-assert-position-constant-comment (position) + (context-coloring-test-assert-position-face position '(font-lock-constant-face + font-lock-comment-face))) + +(defun context-coloring-test-assert-position-string (position) + (context-coloring-test-assert-position-face position 'font-lock-string-face)) + +(defun context-coloring-test-assert-position-nil (position) + (context-coloring-test-assert-position-face position nil)) + +(defun context-coloring-test-assert-coloring (map) + "Assert that the current buffer's coloring matches MAP." + ;; Omit the superfluous, formatting-related leading newline. Can't use + ;; `save-excursion' here because if an assertion fails it will cause future + ;; tests to get messed up. + (goto-char (point-min)) + (let* ((map (substring map 1)) + (index 0) + char-string + char) + (while (< index (length map)) + (setq char-string (substring map index (1+ index))) + (setq char (string-to-char char-string)) + (cond + ;; Newline + ((= char 10) + (next-logical-line) + (beginning-of-line)) + ;; Number + ((and (>= char 48) + (<= char 57)) + (context-coloring-test-assert-position-level + (point) (string-to-number char-string)) + (forward-char)) + ;; ';' = Comment + ((= char 59) + (context-coloring-test-assert-position-comment (point)) + (forward-char)) + ;; 'c' = Constant comment + ((= char 99) + (context-coloring-test-assert-position-constant-comment (point)) + (forward-char)) + ;; 'n' = nil + ((= char 110) + (context-coloring-test-assert-position-nil (point)) + (forward-char)) + ;; 's' = String + ((= char 115) + (context-coloring-test-assert-position-string (point)) + (forward-char)) + (t + (forward-char))) + (setq index (1+ index))))) + (defmacro context-coloring-test-assert-region (&rest body) "Assert something about the face of points in a region. Provides the free variables `i', `length', `point', `face' and @@ -235,8 +348,16 @@ EXPECTED-FACE." (context-coloring-test-assert-region-face start end 'font-lock-string-face)) +(defun context-coloring-test-get-last-message () + (let ((messages (split-string + (buffer-substring-no-properties + (point-min) + (point-max)) + "\n"))) + (car (nthcdr (- (length messages) 2) messages)))) + (defun context-coloring-test-assert-message (expected buffer) - "Assert that message EXPECTED exists in BUFFER." + "Assert that message EXPECTED is at the end of BUFFER." (when (null (get-buffer buffer)) (ert-fail (format @@ -245,20 +366,28 @@ EXPECTED-FACE." "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)))) - (when (not (equal message expected)) + (let ((message (context-coloring-test-get-last-message))) + (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-not-message (expected buffer) + "Assert that message EXPECTED is not at the end of BUFFER." + (when (get-buffer buffer) + (with-current-buffer buffer + (let ((message (context-coloring-test-get-last-message))) + (when (equal message expected) (ert-fail (format (concat - "Expected buffer `%s' to have message \"%s\", " - "but instead it was \"%s\"") - buffer expected - message))))))) + "Expected buffer `%s' not to have message \"%s\", " + "but it did") + buffer expected))))))) (defun context-coloring-test-assert-no-message (buffer) "Assert that BUFFER has no message." @@ -376,7 +505,7 @@ FOREGROUND. Apply ARGUMENTS to (funcall done))) (insert " ") (set-window-buffer (selected-window) (current-buffer)) - (context-coloring-maybe-colorize))) + (context-coloring-maybe-colorize (current-buffer)))) (context-coloring-mode)))) (ert-deftest context-coloring-test-check-version () @@ -393,6 +522,15 @@ FOREGROUND. Apply ARGUMENTS to "Context coloring is not available for this major mode" "*Messages*"))) +(ert-deftest context-coloring-test-derived-mode () + (context-coloring-test-with-fixture + "./fixtures/empty" + (lisp-interaction-mode) + (context-coloring-mode) + (context-coloring-test-assert-not-message + "Context coloring is not available for this major mode" + "*Messages*"))) + (define-derived-mode context-coloring-test-define-dispatch-error-mode fundamental-mode @@ -988,6 +1126,109 @@ see that function." (context-coloring-test-deftest-js2-mode unterminated-comment) +(context-coloring-test-deftest-emacs-lisp-mode defun + (lambda () + (context-coloring-test-assert-coloring " +111111 000 1111 111 111111111 1111 + 11 111 111 111 000011 + +0000 0 0 00 + +111111 01 +111111 111"))) + +(context-coloring-test-deftest-emacs-lisp-mode lambda + (lambda () + (context-coloring-test-assert-coloring " +00000000 1111111 1111 + 11111111 11 2222222 2222 + 222 22 12 2221 111 0 00"))) + +(context-coloring-test-deftest-emacs-lisp-mode quote + (lambda () + (context-coloring-test-assert-coloring " +(xxxxx x (x) + (xx (xx x 111 + 111111 1 111 111 + 111111 1 1111111111 11 111 1 111 1 00001 10000 11 00001 1 100001111"))) + +(context-coloring-test-deftest-emacs-lisp-mode comment + (lambda () + ;; Just check that the comment isn't parsed syntactically. + (context-coloring-test-assert-coloring " +(xxxxx x () + (xx (x xxxxx-xxxx xx) ;;;;;;;;;; + 11 00000-0000 11))) ;;;;;;;;;;")) + :setup (lambda () + (setq context-coloring-syntactic-comments t))) + +(context-coloring-test-deftest-emacs-lisp-mode string + (lambda () + (context-coloring-test-assert-coloring " +(xxxxx x (x) + (xxxxxx x x sss 1 0 sssss 0 1 sssssss11")) + :setup (lambda () + (setq context-coloring-syntactic-strings t))) + +(context-coloring-test-deftest-emacs-lisp-mode ignored + (lambda () + (context-coloring-test-assert-coloring " +(xxxxx x () + (x x 1 11 11 111 11 1 111 (1 1 1)))"))) + +(context-coloring-test-deftest-emacs-lisp-mode let + (lambda () + (context-coloring-test-assert-coloring " +1111 11 + 11 01 + 11 00001 + 11 2222 22 + 22 02 + 22 000022 + 2222 2 2 2 00002211 + 1111 1 1 1 000011"))) + +(context-coloring-test-deftest-emacs-lisp-mode let* + (lambda () + (context-coloring-test-assert-coloring " +11111 11 + 11 11 + 11 000011 + 1111 1 1 1 0 0 00001 + 22222 22 + 22 12 + 22 00002 + 22 02 + 22 222 + 2222 1 1 2 2 2 000022 + 1111 1 1 1 0 0 000011"))) + +(defun context-coloring-test-insert-unread-space () + (setq unread-command-events (cons '(t . 32) + unread-command-events))) + +(defun context-coloring-test-remove-faces () + (remove-text-properties (point-min) (point-max) '(face nil))) + +(context-coloring-test-deftest-emacs-lisp-mode iteration + (lambda () + (let ((context-coloring-emacs-lisp-iterations-per-pause 1)) + (context-coloring-colorize) + (context-coloring-test-assert-coloring " +;; `cc' `cc' +(xxxxx x ())") + (context-coloring-test-remove-faces) + (context-coloring-test-insert-unread-space) + (context-coloring-colorize) + ;; The first iteration will color the first part of the comment, but + ;; that's it. Then it will be interrupted. + (context-coloring-test-assert-coloring " +;; nnnn nnnn +nnnnnn n nnn"))) + :setup (lambda () + (setq context-coloring-syntactic-comments t) + (setq context-coloring-syntactic-strings t))) + (provide 'context-coloring-test) ;;; context-coloring-test.el ends here diff --git a/test/fixtures/comment.el b/test/fixtures/comment.el new file mode 100644 index 0000000..c3ba432 --- /dev/null +++ b/test/fixtures/comment.el @@ -0,0 +1,3 @@ +(defun a () + (or (= token-char 96) ; 96 = '`' + (= token-char 44))) ; 44 = ',' diff --git a/test/fixtures/defun.el b/test/fixtures/defun.el new file mode 100644 index 0000000..a5bd039 --- /dev/null +++ b/test/fixtures/defun.el @@ -0,0 +1,7 @@ +(defun abc (def ghi &optional jkl) + (+ def ghi jkl free)) + +(abc 1 2 3) + +(defun a) +(defun ()) diff --git a/test/fixtures/ignored.el b/test/fixtures/ignored.el new file mode 100644 index 0000000..776a846 --- /dev/null +++ b/test/fixtures/ignored.el @@ -0,0 +1,2 @@ +(defun a () + (+ a 1 +1 -1 1.0 :a t nil (0 . 0))) diff --git a/test/fixtures/iteration.el b/test/fixtures/iteration.el new file mode 100644 index 0000000..c4e99ac --- /dev/null +++ b/test/fixtures/iteration.el @@ -0,0 +1,2 @@ +;; `aa' `bb' +(defun a ()) diff --git a/test/fixtures/lambda.el b/test/fixtures/lambda.el new file mode 100644 index 0000000..9ab7be2 --- /dev/null +++ b/test/fixtures/lambda.el @@ -0,0 +1,3 @@ +(funcall (lambda (fn a) + (funcall fn (lambda (fn) + (fn fn a) fn)) fn) 0 1) diff --git a/test/fixtures/let*.el b/test/fixtures/let*.el new file mode 100644 index 0000000..44d743c --- /dev/null +++ b/test/fixtures/let*.el @@ -0,0 +1,11 @@ +(let* (a + (b a) + (c free)) + (and a b c d e free) + (let* (d + (e a) + (c free) + (g f) + (f g)) + (and a b c d e free)) + (and a b c d e free)) diff --git a/test/fixtures/let.el b/test/fixtures/let.el new file mode 100644 index 0000000..11637b1 --- /dev/null +++ b/test/fixtures/let.el @@ -0,0 +1,8 @@ +(let (a + (b a) + (c free) + (d (let (a + (b a) + (c free)) + (and a b c free)))) + (and a b c free)) diff --git a/test/fixtures/quote.el b/test/fixtures/quote.el new file mode 100644 index 0000000..654bc70 --- /dev/null +++ b/test/fixtures/quote.el @@ -0,0 +1,4 @@ +(defun a (a) + (or (eq a 'b) + (equal a '(a b)) + (equal a `(,(append () `(a b ,(+ 1 free) ,free b) free) b ,free)))) diff --git a/test/fixtures/string.el b/test/fixtures/string.el new file mode 100644 index 0000000..4172642 --- /dev/null +++ b/test/fixtures/string.el @@ -0,0 +1,2 @@ +(defun a (a) + (concat a b "(" a b "(\"" b a "(\"\""))