]> code.delx.au - gnu-emacs-elpa/blob - packages/context-coloring/context-coloring-javascript.el
Merge commit '3007b2917d71a7d66eb94876536dfd80b0661d40' from context-coloring
[gnu-emacs-elpa] / packages / context-coloring / 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 (defsubst context-coloring-js2-scope-level (scope initial)
46 "Return the level of SCOPE, starting from INITIAL."
47 (cond ((gethash scope context-coloring-js2-scope-level-hash-table))
48 (t
49 (let ((level initial)
50 (current-scope scope)
51 enclosing-scope)
52 (while (and current-scope
53 (js2-node-parent current-scope)
54 (setq enclosing-scope
55 (js2-node-get-enclosing-scope current-scope)))
56 (when (or context-coloring-javascript-block-scopes
57 (let ((type (js2-scope-type current-scope)))
58 (or (= type js2-SCRIPT)
59 (= type js2-FUNCTION)
60 (= type js2-CATCH))))
61 (setq level (+ level 1)))
62 (setq current-scope enclosing-scope))
63 (puthash scope level context-coloring-js2-scope-level-hash-table)))))
64
65 (defsubst context-coloring-js2-local-name-node-p (node)
66 "Determine if NODE represents a local variable."
67 (and (js2-name-node-p node)
68 (let ((parent (js2-node-parent node)))
69 (not (or (and (js2-object-prop-node-p parent)
70 (eq node (js2-object-prop-node-left parent)))
71 (and (js2-prop-get-node-p parent)
72 ;; For nested property lookup, the node on the left is a
73 ;; `js2-prop-get-node', so this always works.
74 (eq node (js2-prop-get-node-right parent))))))))
75
76 (defvar-local context-coloring-point-max nil
77 "Cached value of `point-max'.")
78
79 (defsubst context-coloring-js2-colorize-node (node level)
80 "Color NODE with the color for LEVEL."
81 (let ((start (js2-node-abs-pos node)))
82 (context-coloring-colorize-region
83 start
84 (min
85 ;; End
86 (+ start (js2-node-len node))
87 ;; Somes nodes (like the ast when there is an unterminated multiline
88 ;; comment) will stretch to the value of `point-max'.
89 context-coloring-point-max)
90 level)))
91
92 (defun context-coloring-js2-colorize-ast ()
93 "Color the buffer using the `js2-mode' abstract syntax tree."
94 ;; Reset the hash table; the old one could be obsolete.
95 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test #'eq))
96 (setq context-coloring-point-max (point-max))
97 (with-silent-modifications
98 (js2-visit-ast
99 js2-mode-ast
100 (lambda (node end-p)
101 (when (null end-p)
102 (cond
103 ((js2-scope-p node)
104 (context-coloring-js2-colorize-node
105 node
106 (context-coloring-js2-scope-level node context-coloring-initial-level)))
107 ((context-coloring-js2-local-name-node-p node)
108 (let* ((enclosing-scope (js2-node-get-enclosing-scope node))
109 (defining-scope (js2-get-defining-scope
110 enclosing-scope
111 (js2-name-node-name node))))
112 ;; The tree seems to be walked lexically, so an entire scope will
113 ;; be colored, including its name nodes, before they are reached.
114 ;; Coloring the nodes defined in that scope would be redundant, so
115 ;; don't do it.
116 (when (not (eq defining-scope enclosing-scope))
117 (context-coloring-js2-colorize-node
118 node
119 ;; Use `0' as an initial level so global variables are always at
120 ;; the highest level (even if `context-coloring-initial-level'
121 ;; specifies an initial level for the rest of the code).
122 (context-coloring-js2-scope-level defining-scope 0))))))
123 ;; The `t' indicates to search children.
124 t)))
125 (context-coloring-colorize-comments-and-strings)))
126
127 (defconst context-coloring-node-comment-regexp
128 (concat
129 ;; Ensure the "//" or "/*" comment starts with the directive.
130 "\\(//[[:space:]]*\\|/\\*[[:space:]]*\\)"
131 ;; Support multiple directive formats.
132 "\\("
133 ;; JSLint and JSHint support a JSON-like format.
134 "\\(jslint\\|jshint\\)[[:space:]].*?node:[[:space:]]*true"
135 "\\|"
136 ;; ESLint just specifies the option name.
137 "eslint-env[[:space:]].*?node"
138 "\\)")
139 "Match a comment body hinting at a Node.js program.")
140
141 (defun context-coloring-js2-top-level-local-p ()
142 "Guess whether top-level variables are local.
143 For instance, the current file could be a Node.js program."
144 (or
145 ;; A shebang is a pretty obvious giveaway.
146 (string-equal
147 "node"
148 (save-excursion
149 (goto-char (point-min))
150 (when (looking-at auto-mode-interpreter-regexp)
151 (match-string 2))))
152 ;; Otherwise, perform static analysis.
153 (progn
154 (setq context-coloring-js2-scope-level-hash-table (make-hash-table :test #'eq))
155 (catch 'node-program-p
156 (js2-visit-ast
157 js2-mode-ast
158 (lambda (node end-p)
159 (when (null end-p)
160 (when
161 (cond
162 ;; Infer based on inline linter configuration.
163 ((js2-comment-node-p node)
164 (string-match-p
165 context-coloring-node-comment-regexp
166 (js2-node-string node)))
167 ;; Infer based on the prescence of certain variables.
168 ((and (js2-name-node-p node)
169 (let ((parent (js2-node-parent node)))
170 (not (and (js2-object-prop-node-p parent)
171 (eq node (js2-object-prop-node-left parent))))))
172 (let ((name (js2-name-node-name node))
173 (parent (js2-node-parent node)))
174 (and
175 (cond
176 ;; Check whether this is "exports.something" or
177 ;; "module.exports".
178 ((js2-prop-get-node-p parent)
179 (and
180 (eq node (js2-prop-get-node-left parent))
181 (or (string-equal name "exports")
182 (let* ((property (js2-prop-get-node-right parent))
183 (property-name (js2-name-node-name property)))
184 (and (string-equal name "module")
185 (string-equal property-name "exports"))))))
186 ;; Check whether it's a "require('module')" call.
187 ((js2-call-node-p parent)
188 (or (string-equal name "require"))))
189 (let* ((enclosing-scope (js2-node-get-enclosing-scope node))
190 (defining-scope (js2-get-defining-scope
191 enclosing-scope name)))
192 ;; The variable also must be global.
193 (null defining-scope))))))
194 (throw 'node-program-p t))
195 ;; The `t' indicates to search children.
196 t)))
197 ;; Default to returning nil from the catch body.
198 nil))))
199
200 (defcustom context-coloring-javascript-detect-top-level-scope t
201 "If non-nil, detect when to use file-level scope."
202 :type 'boolean
203 :group 'context-coloring)
204
205 ;;;###autoload
206 (defun context-coloring-js2-colorize ()
207 "Color the buffer using the `js2-mode'."
208 (cond
209 ;; Increase the initial level if we should.
210 ((and context-coloring-javascript-detect-top-level-scope
211 (context-coloring-js2-top-level-local-p))
212 (let ((context-coloring-initial-level 1))
213 (context-coloring-js2-colorize-ast)))
214 (t
215 (context-coloring-js2-colorize-ast))))
216
217 ;;;###autoload
218 (puthash
219 'javascript
220 (list :modes '(js2-mode js2-jsx-mode)
221 :colorizer #'context-coloring-js2-colorize
222 :setup
223 (lambda ()
224 (add-hook 'js2-post-parse-callbacks #'context-coloring-colorize nil t))
225 :teardown
226 (lambda ()
227 (remove-hook 'js2-post-parse-callbacks #'context-coloring-colorize t)))
228 context-coloring-dispatch-hash-table)
229
230 (provide 'context-coloring-javascript)
231
232 ;;; context-coloring-javascript.el ends here