1 ;;; context-coloring-javascript.el --- JavaScript support -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2014-2016 Free Software Foundation, Inc.
5 ;; This file is part of GNU Emacs.
7 ;; This program is free software; you can redistribute it and/or modify
8 ;; it under the terms of the GNU General Public License as published by
9 ;; the Free Software Foundation, either version 3 of the License, or
10 ;; (at your option) any later version.
12 ;; This program is distributed in the hope that it will be useful,
13 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ;; GNU General Public License for more details.
17 ;; You should have received a copy of the GNU General Public License
18 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
22 ;; Add JavaScript context coloring support with js2-mode.
26 (require 'context-coloring)
30 ;;; JavaScript colorization
32 (defvar-local context-coloring-js2-scope-level-hash-table nil
33 "Associate `js2-scope' structures and with their scope
36 (defcustom context-coloring-javascript-block-scopes nil
37 "If non-nil, also color block scopes in the scope hierarchy in JavaScript.
39 The block-scoped `let' and `const' are introduced in ES6. Enable
40 this for ES6 code; disable it elsewhere."
43 :group 'context-coloring)
45 (make-obsolete-variable
46 'context-coloring-js-block-scopes
47 'context-coloring-javascript-block-scopes
50 (defsubst context-coloring-js2-scope-level (scope initial)
51 "Return the level of SCOPE, starting from INITIAL."
52 (cond ((gethash scope context-coloring-js2-scope-level-hash-table))
57 (while (and current-scope
58 (js2-node-parent current-scope)
60 (js2-node-get-enclosing-scope current-scope)))
61 (when (or context-coloring-javascript-block-scopes
62 (let ((type (js2-scope-type current-scope)))
63 (or (= type js2-SCRIPT)
66 (setq level (+ level 1)))
67 (setq current-scope enclosing-scope))
68 (puthash scope level context-coloring-js2-scope-level-hash-table)))))
70 (defsubst context-coloring-js2-local-name-node-p (node)
71 "Determine if NODE represents a local variable."
72 (and (js2-name-node-p node)
73 (let ((parent (js2-node-parent node)))
74 (not (or (and (js2-object-prop-node-p parent)
75 (eq node (js2-object-prop-node-left parent)))
76 (and (js2-prop-get-node-p parent)
77 ;; For nested property lookup, the node on the left is a
78 ;; `js2-prop-get-node', so this always works.
79 (eq node (js2-prop-get-node-right parent))))))))
81 (defvar-local context-coloring-point-max nil
82 "Cached value of `point-max'.")
84 (defsubst context-coloring-js2-colorize-node (node level)
85 "Color NODE with the color for LEVEL."
86 (let ((start (js2-node-abs-pos node)))
87 (context-coloring-colorize-region
91 (+ start (js2-node-len node))
92 ;; Somes nodes (like the ast when there is an unterminated multiline
93 ;; comment) will stretch to the value of `point-max'.
94 context-coloring-point-max)
97 (defun context-coloring-js2-colorize-ast ()
98 "Color the buffer using the `js2-mode' abstract syntax tree."
99 ;; Reset the hash table; the old one could be obsolete.
100 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test #'eq))
101 (setq context-coloring-point-max (point-max))
102 (with-silent-modifications
109 (context-coloring-js2-colorize-node
111 (context-coloring-js2-scope-level node context-coloring-initial-level)))
112 ((context-coloring-js2-local-name-node-p node)
113 (let* ((enclosing-scope (js2-node-get-enclosing-scope node))
114 (defining-scope (js2-get-defining-scope
116 (js2-name-node-name node))))
117 ;; The tree seems to be walked lexically, so an entire scope will
118 ;; be colored, including its name nodes, before they are reached.
119 ;; Coloring the nodes defined in that scope would be redundant, so
121 (when (not (eq defining-scope enclosing-scope))
122 (context-coloring-js2-colorize-node
124 ;; Use `0' as an initial level so global variables are always at
125 ;; the highest level (even if `context-coloring-initial-level'
126 ;; specifies an initial level for the rest of the code).
127 (context-coloring-js2-scope-level defining-scope 0))))))
128 ;; The `t' indicates to search children.
130 (context-coloring-colorize-comments-and-strings)))
132 (defconst context-coloring-node-comment-regexp
134 ;; Ensure the "//" or "/*" comment starts with the directive.
135 "\\(//[[:space:]]*\\|/\\*[[:space:]]*\\)"
136 ;; Support multiple directive formats.
138 ;; JSLint and JSHint support a JSON-like format.
139 "\\(jslint\\|jshint\\)[[:space:]].*?node:[[:space:]]*true"
141 ;; ESLint just specifies the option name.
142 "eslint-env[[:space:]].*?node"
144 "Match a comment body hinting at a Node.js program.")
146 ;; TODO: Add ES6 module detection.
147 (defun context-coloring-js2-top-level-local-p ()
148 "Guess whether top-level variables are local.
149 For instance, the current file could be a Node.js program."
151 ;; A shebang is a pretty obvious giveaway.
155 (goto-char (point-min))
156 (when (looking-at auto-mode-interpreter-regexp)
158 ;; Otherwise, perform static analysis.
160 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test #'eq))
161 (catch 'node-program-p
168 ;; Infer based on inline linter configuration.
169 ((js2-comment-node-p node)
171 context-coloring-node-comment-regexp
172 (js2-node-string node)))
173 ;; Infer based on the prescence of certain variables.
174 ((and (js2-name-node-p node)
175 (let ((parent (js2-node-parent node)))
176 (not (and (js2-object-prop-node-p parent)
177 (eq node (js2-object-prop-node-left parent))))))
178 (let ((name (js2-name-node-name node))
179 (parent (js2-node-parent node)))
182 ;; Check whether this is "exports.something" or
184 ((js2-prop-get-node-p parent)
186 (eq node (js2-prop-get-node-left parent))
187 (or (string-equal name "exports")
188 (let* ((property (js2-prop-get-node-right parent))
189 (property-name (js2-name-node-name property)))
190 (and (string-equal name "module")
191 (string-equal property-name "exports"))))))
192 ;; Check whether it's a "require('module')" call.
193 ((js2-call-node-p parent)
194 (or (string-equal name "require"))))
195 (let* ((enclosing-scope (js2-node-get-enclosing-scope node))
196 (defining-scope (js2-get-defining-scope
197 enclosing-scope name)))
198 ;; The variable also must be global.
199 (null defining-scope))))))
200 (throw 'node-program-p t))
201 ;; The `t' indicates to search children.
203 ;; Default to returning nil from the catch body.
206 (defcustom context-coloring-javascript-detect-top-level-scope t
207 "If non-nil, detect when to use file-level scope."
209 :group 'context-coloring)
212 (defun context-coloring-js2-colorize ()
213 "Color the buffer using the `js2-mode'."
215 ;; Increase the initial level if we should.
216 ((and context-coloring-javascript-detect-top-level-scope
217 (context-coloring-js2-top-level-local-p))
218 (let ((context-coloring-initial-level 1))
219 (context-coloring-js2-colorize-ast)))
221 (context-coloring-js2-colorize-ast))))
226 (list :modes '(js2-mode js2-jsx-mode)
227 :colorizer #'context-coloring-js2-colorize
230 (add-hook 'js2-post-parse-callbacks #'context-coloring-colorize nil t))
233 (remove-hook 'js2-post-parse-callbacks #'context-coloring-colorize t)))
234 context-coloring-dispatch-hash-table)
236 (provide 'context-coloring-javascript)
238 ;;; context-coloring-javascript.el ends here