branch: master commit 6fe9e585b033426d2abceb78d7afaec1efe67dd3 Author: Jackson Ray Hamilton <jack...@jacksonrayhamilton.com> Commit: Jackson Ray Hamilton <jack...@jacksonrayhamilton.com>
Add Node.js detection. --- context-coloring.el | 87 +++++++++++++++++++++++++++++++++++++++- test/context-coloring-test.el | 48 ++++++++++++++++++++- test/fixtures/global.js | 2 +- test/fixtures/initial-level.js | 3 +- 4 files changed, 133 insertions(+), 7 deletions(-) diff --git a/context-coloring.el b/context-coloring.el index 902a9db..a2cd88f 100644 --- a/context-coloring.el +++ b/context-coloring.el @@ -298,7 +298,79 @@ are scoped to a file (as in Node.js), set this to `1'." :group 'context-coloring) -;;; js2-mode colorization +;;; Node.js colorization + +(defconst context-coloring-node-comment-regexp + (concat + ;; Ensure the "//" or "/*" comment starts with the directive. + "\\(//[[:space:]]*\\|/\\*[[:space:]]*\\)" + ;; Support multiple directive formats. + "\\(" + ;; JSLint and JSHint support a JSON-like format. + "\\(jslint\\|jshint\\)[[:space:]].*?node:[[:space:]]*true" + "\\|" + ;; ESLint just specifies the option name. + "eslint-env[[:space:]].*?node" + "\\)") + "Match a comment body hinting at a Node.js program.") + +(defun context-coloring-node-program-p () + "Guess whether the current file is a Node.js program." + (or + ;; A shebang is a pretty obvious giveaway. + (string-equal + "node" + (save-excursion + (goto-char (point-min)) + (when (looking-at auto-mode-interpreter-regexp) + (match-string 2)))) + ;; Otherwise, perform static analysis. + (catch 'node-program-p + (js2-visit-ast + js2-mode-ast + (lambda (node end-p) + (when (null end-p) + (when + (cond + ;; Infer based on inline linter configuration. + ((js2-comment-node-p node) + (string-match-p + context-coloring-node-comment-regexp + (js2-node-string node))) + ;; Infer based on the prescence of certain variables. + ((and (js2-name-node-p node) + (let ((parent (js2-node-parent node))) + (not (and (js2-object-prop-node-p parent) + (eq node (js2-object-prop-node-left parent)))))) + (let ((name (js2-name-node-name node)) + (parent (js2-node-parent node))) + (cond + ;; Check whether this is "exports.something" or + ;; "module.exports". + ((js2-prop-get-node-p parent) + (and + (eq node (js2-prop-get-node-left parent)) + (or (string-equal name "exports") + (let* ((property (js2-prop-get-node-right parent)) + (property-name (js2-name-node-name property))) + (or (and (string-equal name "module") + (string-equal property-name "exports"))))))) + ;; Check whether it's a "require('module')" call. + ((js2-call-node-p parent) + (or (string-equal name "require"))))))) + (throw 'node-program-p t)) + ;; The `t' indicates to search children. + t))) + ;; Default to returning nil from the catch body. + nil))) + +(defcustom context-coloring-detect-node t + "If non-nil, use file-level scope for variables in Node.js." + :type 'boolean + :group 'context-coloring) + + +;;; JavaScript colorization (defvar-local context-coloring-js2-scope-level-hash-table nil "Associate `js2-scope' structures and with their scope @@ -365,7 +437,7 @@ this for ES6 code; disable it elsewhere." context-coloring-point-max) level))) -(defun context-coloring-js2-colorize () +(defun context-coloring-js2-colorize-ast () "Color the buffer using the `js2-mode' abstract syntax tree." ;; Reset the hash table; the old one could be obsolete. (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test #'eq)) @@ -400,6 +472,17 @@ this for ES6 code; disable it elsewhere." t))) (context-coloring-colorize-comments-and-strings))) +(defun context-coloring-js2-colorize () + "Color the buffer using the `js2-mode'." + (cond + ;; Increase the initial level if we can detect Node.js. + ((and context-coloring-detect-node + (context-coloring-node-program-p)) + (let ((context-coloring-initial-level 1)) + (context-coloring-js2-colorize-ast))) + (t + (context-coloring-js2-colorize-ast)))) + ;;; Emacs Lisp colorization diff --git a/test/context-coloring-test.el b/test/context-coloring-test.el index c8574da..abd4df1 100644 --- a/test/context-coloring-test.el +++ b/test/context-coloring-test.el @@ -493,7 +493,7 @@ other non-letters are guaranteed to always be discarded." (lambda () (context-coloring-test-assert-coloring " (xxxxxxxx () { - 111 1 1 00000001xxx11 + 111 1 1 0000001xxx11 }());"))) (context-coloring-test-deftest-javascript block-scopes @@ -602,16 +602,58 @@ ssssssssssss0")) ;; As long as `add-text-properties' doesn't signal an error, this test passes. (lambda ())) +(defun context-coloring-test-assert-javascript-elevated-level () + "Assert that the \"initial-level.js\" file has elevated scope." + (context-coloring-test-assert-coloring " + +111 1 1 0000001xxx11")) + +(defun context-coloring-test-assert-javascript-global-level () + "Assert that the \"initial-level.js\" file has global scope." + (context-coloring-test-assert-coloring " + +000 0 0 0000000xxx00")) + (context-coloring-test-deftest-javascript initial-level (lambda () - (context-coloring-test-assert-coloring " -111 1 1 00000001xxx11")) + (context-coloring-test-assert-javascript-elevated-level)) :fixture "initial-level.js" :before (lambda () (setq context-coloring-initial-level 1)) :after (lambda () (setq context-coloring-initial-level 0))) +(defun context-coloring-test-setup-detect-node (string) + "Make STRING the first line and colorize again." + (goto-char (point-min)) + (kill-whole-line 0) + (insert string) + ;; Reparsing triggers recoloring. + (js2-reparse)) + +(context-coloring-test-deftest-javascript detect-node + (lambda () + (let ((positive-indicators + (list "#!/usr/bin/env node" + "/*jslint node: true */" + "// jshint node: true" + "/*eslint-env node */" + "module.exports" + "module.exports.a" + "exports.a" + "require('a')")) + (negative-indicators + (list "// Blah blah jshint blah." + "module" + "exports"))) + (dolist (indicator positive-indicators) + (context-coloring-test-setup-detect-node indicator) + (context-coloring-test-assert-javascript-elevated-level)) + (dolist (indicator negative-indicators) + (context-coloring-test-setup-detect-node indicator) + (context-coloring-test-assert-javascript-global-level)))) + :fixture "initial-level.js") + (context-coloring-test-deftest-emacs-lisp defun (lambda () (context-coloring-test-assert-coloring " diff --git a/test/fixtures/global.js b/test/fixtures/global.js index a35619d..3de2147 100644 --- a/test/fixtures/global.js +++ b/test/fixtures/global.js @@ -1,3 +1,3 @@ (function () { - var a = require('a'); + var a = global('a'); }()); diff --git a/test/fixtures/initial-level.js b/test/fixtures/initial-level.js index 119773e..24a4b71 100644 --- a/test/fixtures/initial-level.js +++ b/test/fixtures/initial-level.js @@ -1 +1,2 @@ -var a = require('a'); + +var a = global('a');