]> code.delx.au - gnu-emacs-elpa/blob - context-coloring-javascript.el
Autoload dispatches.
[gnu-emacs-elpa] / context-coloring-javascript.el
1 ;;; context-coloring-javascript.el --- JavaScript support -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2014-2016 Free Software Foundation, Inc.
4
5 ;; This file is part of GNU Emacs.
6
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.
11
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.
16
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/>.
19
20 ;;; Commentary:
21
22 ;; Add JavaScript context coloring support with js2-mode.
23
24 ;;; Code:
25
26 (require 'context-coloring)
27 (require 'js2-mode)
28
29
30 ;;; JavaScript colorization
31
32 (defvar-local context-coloring-js2-scope-level-hash-table nil
33 "Associate `js2-scope' structures and with their scope
34 levels.")
35
36 (defcustom context-coloring-javascript-block-scopes nil
37 "If non-nil, also color block scopes in the scope hierarchy in JavaScript.
38
39 The block-scoped `let' and `const' are introduced in ES6. Enable
40 this for ES6 code; disable it elsewhere."
41 :type 'boolean
42 :safe #'booleanp
43 :group 'context-coloring)
44
45 (make-obsolete-variable
46 'context-coloring-js-block-scopes
47 'context-coloring-javascript-block-scopes
48 "7.0.0")
49
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))
53 (t
54 (let ((level initial)
55 (current-scope scope)
56 enclosing-scope)
57 (while (and current-scope
58 (js2-node-parent current-scope)
59 (setq enclosing-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)
64 (= type js2-FUNCTION)
65 (= type js2-CATCH))))
66 (setq level (+ level 1)))
67 (setq current-scope enclosing-scope))
68 (puthash scope level context-coloring-js2-scope-level-hash-table)))))
69
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))))))))
80
81 (defvar-local context-coloring-point-max nil
82 "Cached value of `point-max'.")
83
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
88 start
89 (min
90 ;; End
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)
95 level)))
96
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
103 (js2-visit-ast
104 js2-mode-ast
105 (lambda (node end-p)
106 (when (null end-p)
107 (cond
108 ((js2-scope-p node)
109 (context-coloring-js2-colorize-node
110 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
115 enclosing-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
120 ;; don't do it.
121 (when (not (eq defining-scope enclosing-scope))
122 (context-coloring-js2-colorize-node
123 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.
129 t)))
130 (context-coloring-colorize-comments-and-strings)))
131
132 (defconst context-coloring-node-comment-regexp
133 (concat
134 ;; Ensure the "//" or "/*" comment starts with the directive.
135 "\\(//[[:space:]]*\\|/\\*[[:space:]]*\\)"
136 ;; Support multiple directive formats.
137 "\\("
138 ;; JSLint and JSHint support a JSON-like format.
139 "\\(jslint\\|jshint\\)[[:space:]].*?node:[[:space:]]*true"
140 "\\|"
141 ;; ESLint just specifies the option name.
142 "eslint-env[[:space:]].*?node"
143 "\\)")
144 "Match a comment body hinting at a Node.js program.")
145
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."
150 (or
151 ;; A shebang is a pretty obvious giveaway.
152 (string-equal
153 "node"
154 (save-excursion
155 (goto-char (point-min))
156 (when (looking-at auto-mode-interpreter-regexp)
157 (match-string 2))))
158 ;; Otherwise, perform static analysis.
159 (progn
160 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test #'eq))
161 (catch 'node-program-p
162 (js2-visit-ast
163 js2-mode-ast
164 (lambda (node end-p)
165 (when (null end-p)
166 (when
167 (cond
168 ;; Infer based on inline linter configuration.
169 ((js2-comment-node-p node)
170 (string-match-p
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)))
180 (and
181 (cond
182 ;; Check whether this is "exports.something" or
183 ;; "module.exports".
184 ((js2-prop-get-node-p parent)
185 (and
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.
202 t)))
203 ;; Default to returning nil from the catch body.
204 nil))))
205
206 (defcustom context-coloring-javascript-detect-top-level-scope t
207 "If non-nil, detect when to use file-level scope."
208 :type 'boolean
209 :group 'context-coloring)
210
211 ;;;###autoload
212 (defun context-coloring-js2-colorize ()
213 "Color the buffer using the `js2-mode'."
214 (cond
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)))
220 (t
221 (context-coloring-js2-colorize-ast))))
222
223 ;;;###autoload
224 (puthash
225 'javascript
226 (list :modes '(js2-mode js2-jsx-mode)
227 :colorizer #'context-coloring-js2-colorize
228 :setup
229 (lambda ()
230 (add-hook 'js2-post-parse-callbacks #'context-coloring-colorize nil t))
231 :teardown
232 (lambda ()
233 (remove-hook 'js2-post-parse-callbacks #'context-coloring-colorize t)))
234 context-coloring-dispatch-hash-table)
235
236 (provide 'context-coloring-javascript)
237
238 ;;; context-coloring-javascript.el ends here