Showing
13 changed files
with
2008 additions
and
0 deletions
1 | +;;; js2-imenu-extras.el --- Imenu support for additional constructs | ||
2 | + | ||
3 | +;; Copyright (C) 2012-2014 Free Software Foundation, Inc. | ||
4 | + | ||
5 | +;; Author: Dmitry Gutov <dgutov@yandex.ru> | ||
6 | +;; Keywords: languages, javascript, imenu | ||
7 | + | ||
8 | +;; This file is part of GNU Emacs. | ||
9 | + | ||
10 | +;; GNU Emacs is free software: you can redistribute it and/or modify | ||
11 | +;; it under the terms of the GNU General Public License as published by | ||
12 | +;; the Free Software Foundation, either version 3 of the License, or | ||
13 | +;; (at your option) any later version. | ||
14 | + | ||
15 | +;; GNU Emacs is distributed in the hope that it will be useful, | ||
16 | +;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
17 | +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
18 | +;; GNU General Public License for more details. | ||
19 | + | ||
20 | +;; You should have received a copy of the GNU General Public License | ||
21 | +;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. | ||
22 | + | ||
23 | +;;; Commentary: | ||
24 | + | ||
25 | +;; This package adds Imenu support for additional framework constructs and | ||
26 | +;; structural patterns to `js2-mode'. | ||
27 | + | ||
28 | +;; Usage: | ||
29 | + | ||
30 | +;; (add-hook 'js2-mode-hook 'js2-imenu-extras-mode) | ||
31 | + | ||
32 | +;; To customize how it works: | ||
33 | +;; M-x customize-group RET js2-imenu RET | ||
34 | + | ||
35 | +(eval-when-compile | ||
36 | + (require 'cl)) | ||
37 | + | ||
38 | +(require 'js2-mode) | ||
39 | + | ||
40 | +(defvar js2-imenu-extension-styles | ||
41 | + `((:framework jquery | ||
42 | + :call-re "\\_<\\(?:jQuery\\|\\$\\|_\\)\\.extend\\s-*(" | ||
43 | + :recorder js2-imenu-record-jquery-extend) | ||
44 | + | ||
45 | + (:framework jquery-ui | ||
46 | + :call-re "^\\s-*\\(?:jQuery\\|\\$\\)\\.widget\\s-*(" | ||
47 | + :recorder js2-imenu-record-string-declare) | ||
48 | + | ||
49 | + (:framework dojo | ||
50 | + :call-re "^\\s-*dojo.declare\\s-*(" | ||
51 | + :recorder js2-imenu-record-string-declare) | ||
52 | + | ||
53 | + (:framework backbone | ||
54 | + :call-re ,(concat "\\_<" js2-mode-identifier-re "\\.extend\\s-*(") | ||
55 | + :recorder js2-imenu-record-backbone-extend) | ||
56 | + | ||
57 | + (:framework enyo | ||
58 | + :call-re "\\_<enyo\\.kind\\s-*(" | ||
59 | + :recorder js2-imenu-record-enyo-kind) | ||
60 | + | ||
61 | + (:framework react | ||
62 | + :call-re "\\_<React\\.createClass\\s-*(" | ||
63 | + :recorder js2-imenu-record-react-class) | ||
64 | + | ||
65 | + (:framework sencha | ||
66 | + :call-re "^\\s-*Ext\\.define\\s-*(" | ||
67 | + :recorder js2-imenu-record-sencha-class)) | ||
68 | + "List of JavaScript class definition or extension styles. | ||
69 | + | ||
70 | +:framework is a valid value in `js2-imenu-enabled-frameworks'. | ||
71 | + | ||
72 | +:call-re is a regular expression that has no capturing groups. | ||
73 | + | ||
74 | +:recorder is a function name that will be called when the regular | ||
75 | +expression matches some text in the buffer. When it's called, point will be | ||
76 | +at the end of the match. The function must keep the point position.") | ||
77 | + | ||
78 | +(defconst js2-imenu-available-frameworks | ||
79 | + (mapcar (lambda (style) (plist-get style :framework)) js2-imenu-extension-styles) | ||
80 | + "List of available JavaScript framework symbols.") | ||
81 | + | ||
82 | +(defcustom js2-imenu-enabled-frameworks js2-imenu-available-frameworks | ||
83 | + "Frameworks to be recognized by `js2-mode'." | ||
84 | + :type (cons 'set (mapcar (lambda (x) (list 'const x)) | ||
85 | + js2-imenu-available-frameworks)) | ||
86 | + :group 'js2-imenu) | ||
87 | + | ||
88 | +(defcustom js2-imenu-show-other-functions t | ||
89 | + "Non-nil to show functions not recognized by other mechanisms, | ||
90 | +in a shared namespace." | ||
91 | + :type 'boolean | ||
92 | + :group 'js2-imenu) | ||
93 | + | ||
94 | +(defcustom js2-imenu-other-functions-ns "?" | ||
95 | + "Namespace name to use for other functions." | ||
96 | + :type 'string | ||
97 | + :group 'js2-imenu) | ||
98 | + | ||
99 | +(defcustom js2-imenu-show-module-pattern t | ||
100 | + "Non-nil to recognize the module pattern: | ||
101 | + | ||
102 | +var foobs = (function(a) { | ||
103 | + return {fib: function() {}, fub: function() {}}; | ||
104 | +})(b); | ||
105 | + | ||
106 | +We record the returned hash as belonging to the named module, and | ||
107 | +prefix any functions defined inside the IIFE with the module name." | ||
108 | + :type 'boolean | ||
109 | + :group 'js2-imenu) | ||
110 | + | ||
111 | +(defcustom js2-imenu-split-string-identifiers t | ||
112 | + "When non-nil, split string identifiers on dots. | ||
113 | +Currently used for jQuery widgets, Dojo and Enyo declarations." | ||
114 | + :type 'boolean | ||
115 | + :group 'js2-imenu) | ||
116 | + | ||
117 | +;;;###autoload | ||
118 | +(defun js2-imenu-extras-setup () | ||
119 | + (when js2-imenu-enabled-frameworks | ||
120 | + (add-hook 'js2-build-imenu-callbacks 'js2-imenu-record-declarations t t)) | ||
121 | + (when (or js2-imenu-show-other-functions js2-imenu-show-module-pattern) | ||
122 | + (add-hook 'js2-build-imenu-callbacks 'js2-imenu-walk-ast t t))) | ||
123 | + | ||
124 | +(defun js2-imenu-extras-remove () | ||
125 | + (remove-hook 'js2-build-imenu-callbacks 'js2-imenu-record-declarations t) | ||
126 | + (remove-hook 'js2-build-imenu-callbacks 'js2-imenu-walk-ast t)) | ||
127 | + | ||
128 | +(defun js2-imenu-record-declarations () | ||
129 | + (let* ((styles (loop for style in js2-imenu-extension-styles | ||
130 | + when (memq (plist-get style :framework) | ||
131 | + js2-imenu-enabled-frameworks) | ||
132 | + collect style)) | ||
133 | + (re (mapconcat (lambda (style) | ||
134 | + (concat "\\(" (plist-get style :call-re) "\\)")) | ||
135 | + styles "\\|"))) | ||
136 | + (goto-char (point-min)) | ||
137 | + (while (js2-re-search-forward re nil t) | ||
138 | + (loop for i from 0 to (1- (length styles)) | ||
139 | + when (match-beginning (1+ i)) | ||
140 | + return (funcall (plist-get (nth i styles) :recorder)))))) | ||
141 | + | ||
142 | +(defun js2-imenu-record-jquery-extend () | ||
143 | + (let ((pred (lambda (subject) | ||
144 | + (and | ||
145 | + (js2-prop-get-node-p subject) | ||
146 | + (string= (js2-name-node-name (js2-prop-get-node-right subject)) | ||
147 | + "prototype"))))) | ||
148 | + (js2-imenu-record-extend-first-arg (1- (point)) pred | ||
149 | + 'js2-compute-nested-prop-get))) | ||
150 | + | ||
151 | +(defun js2-imenu-record-string-declare () | ||
152 | + (js2-imenu-record-extend-first-arg | ||
153 | + (1- (point)) 'js2-string-node-p | ||
154 | + (lambda (node) | ||
155 | + (if js2-imenu-split-string-identifiers | ||
156 | + (split-string (js2-string-node-value node) "\\." t) | ||
157 | + (list (js2-string-node-value node)))))) | ||
158 | + | ||
159 | +(defun js2-imenu-record-extend-first-arg (point pred qname-fn) | ||
160 | + (let* ((node (js2-node-at-point point)) | ||
161 | + (args (js2-call-node-args node)) | ||
162 | + (subject (first args))) | ||
163 | + (when (funcall pred subject) | ||
164 | + (loop for arg in (cdr args) | ||
165 | + when (js2-object-node-p arg) | ||
166 | + do (js2-record-object-literal | ||
167 | + arg (funcall qname-fn subject) (js2-node-abs-pos arg)))))) | ||
168 | + | ||
169 | +(defun js2-imenu-record-backbone-or-react () | ||
170 | + (let* ((node (js2-node-at-point (1- (point)))) | ||
171 | + (args (js2-call-node-args node)) | ||
172 | + (methods (first args)) | ||
173 | + (parent (js2-node-parent node))) | ||
174 | + (when (js2-object-node-p methods) | ||
175 | + (let ((subject (cond ((js2-var-init-node-p parent) | ||
176 | + (js2-var-init-node-target parent)) | ||
177 | + ((js2-assign-node-p parent) | ||
178 | + (js2-assign-node-left parent))))) | ||
179 | + (when subject | ||
180 | + (js2-record-object-literal methods | ||
181 | + (js2-compute-nested-prop-get subject) | ||
182 | + (js2-node-abs-pos methods))))))) | ||
183 | + | ||
184 | +(defalias 'js2-imenu-record-backbone-extend 'js2-imenu-record-backbone-or-react) | ||
185 | + | ||
186 | +(defalias 'js2-imenu-record-react-class 'js2-imenu-record-backbone-or-react) | ||
187 | + | ||
188 | +(defun js2-imenu-record-enyo-kind () | ||
189 | + (let* ((node (js2-node-at-point (1- (point)))) | ||
190 | + (args (js2-call-node-args node)) | ||
191 | + (options (first args))) | ||
192 | + (when (js2-object-node-p options) | ||
193 | + (let ((name-value | ||
194 | + (loop for elem in (js2-object-node-elems options) | ||
195 | + thereis | ||
196 | + (let ((key (js2-object-prop-node-left elem)) | ||
197 | + (value (js2-object-prop-node-right elem))) | ||
198 | + (when (and (equal | ||
199 | + (cond ((js2-name-node-p key) | ||
200 | + (js2-name-node-name key)) | ||
201 | + ((js2-string-node-p key) | ||
202 | + (js2-string-node-value key))) | ||
203 | + "name") | ||
204 | + (js2-string-node-p value)) | ||
205 | + (js2-string-node-value value)))))) | ||
206 | + (when name-value | ||
207 | + (js2-record-object-literal options | ||
208 | + (if js2-imenu-split-string-identifiers | ||
209 | + (split-string name-value "\\.") | ||
210 | + (list name-value)) | ||
211 | + (js2-node-abs-pos options))))))) | ||
212 | + | ||
213 | +(defun js2-imenu-record-sencha-class () | ||
214 | + (let* ((node (js2-node-at-point (1- (point)))) | ||
215 | + (args (js2-call-node-args node)) | ||
216 | + (name (first args)) | ||
217 | + (methods (second args))) | ||
218 | + (when (and (js2-string-node-p name) (js2-object-node-p methods)) | ||
219 | + (let ((name-value (js2-string-node-value name))) | ||
220 | + (js2-record-object-literal methods | ||
221 | + (if js2-imenu-split-string-identifiers | ||
222 | + (split-string name-value "\\." t) | ||
223 | + (list name-value)) | ||
224 | + (js2-node-abs-pos methods)))))) | ||
225 | + | ||
226 | +(defun js2-imenu-walk-ast () | ||
227 | + (js2-visit-ast | ||
228 | + js2-mode-ast | ||
229 | + (lambda (node end-p) | ||
230 | + (unless end-p | ||
231 | + (cond | ||
232 | + ((and js2-imenu-show-other-functions | ||
233 | + (js2-object-prop-node-p node)) | ||
234 | + (js2-imenu-record-orphan-prop-node-function node)) | ||
235 | + ((js2-assign-node-p node) | ||
236 | + (cond | ||
237 | + ((and js2-imenu-show-other-functions | ||
238 | + (js2-function-node-p | ||
239 | + (js2-assign-node-right node))) | ||
240 | + (js2-imenu-record-orphan-assign-node-function | ||
241 | + (js2-assign-node-left node) | ||
242 | + (js2-assign-node-right node))) | ||
243 | + ((and js2-imenu-show-module-pattern | ||
244 | + (js2-call-node-p | ||
245 | + (js2-assign-node-right node))) | ||
246 | + (js2-imenu-record-module-pattern | ||
247 | + (js2-assign-node-left node) | ||
248 | + (js2-assign-node-right node))))) | ||
249 | + ((js2-var-init-node-p node) | ||
250 | + (cond | ||
251 | + ((and js2-imenu-show-other-functions | ||
252 | + (js2-function-node-p | ||
253 | + (js2-var-init-node-initializer node))) | ||
254 | + (js2-imenu-record-orphan-assign-node-function | ||
255 | + (js2-var-init-node-target node) | ||
256 | + (js2-var-init-node-initializer node))) | ||
257 | + ((and js2-imenu-show-module-pattern | ||
258 | + (js2-call-node-p | ||
259 | + (js2-var-init-node-initializer node))) | ||
260 | + (js2-imenu-record-module-pattern | ||
261 | + (js2-var-init-node-target node) | ||
262 | + (js2-var-init-node-initializer node)))))) | ||
263 | + t)))) | ||
264 | + | ||
265 | +(defun js2-imenu-parent-key-names (node) | ||
266 | + "Get the list of parent key names of NODE. | ||
267 | + | ||
268 | +For example, for code | ||
269 | + | ||
270 | + {rules: {password: {required: function() {}}}} | ||
271 | + | ||
272 | +when NODE is the inner `js2-object-prop-mode', | ||
273 | +it returns `(\"rules\" \"password\")'." | ||
274 | + (let (rlt (n node)) | ||
275 | + (while (setq n (js2-imenu-parent-prop-node n)) | ||
276 | + (push (js2-prop-node-name (js2-object-prop-node-left n)) rlt)) | ||
277 | + rlt)) | ||
278 | + | ||
279 | +(defun js2-imenu-parent-prop-node (node) | ||
280 | + "When the parent of NODE is `js2-object-node', | ||
281 | +and the grandparent is `js2-object-prop-node', | ||
282 | +return the grandparent." | ||
283 | + ;; Suppose the code is: | ||
284 | + ;; {parent-key: {required: function() {}}} | ||
285 | + ;; NODE is `required: function() {}'. | ||
286 | + (let (p2 p3) | ||
287 | + ;; Parent is `{required: function() {}}'. | ||
288 | + (setq p2 (js2-node-parent node)) | ||
289 | + ;; GP is `parent-key: {required: function() {}}'. | ||
290 | + (when (and p2 (js2-object-node-p p2)) | ||
291 | + (setq p3 (js2-node-parent p2)) | ||
292 | + (if (and p3 (js2-object-prop-node-p p3)) p3)))) | ||
293 | + | ||
294 | +(defun js2-imenu-record-orphan-prop-node-function (node) | ||
295 | + "Record orphan function when it's the value of NODE. | ||
296 | +NODE must be `js2-object-prop-node'." | ||
297 | + (when (js2-function-node-p (js2-object-prop-node-right node)) | ||
298 | + (let ((fn-node (js2-object-prop-node-right node))) | ||
299 | + (unless (and js2-imenu-function-map | ||
300 | + (gethash fn-node js2-imenu-function-map)) | ||
301 | + (let ((key-node (js2-object-prop-node-left node)) | ||
302 | + (parent-prop-node (js2-imenu-parent-prop-node node)) | ||
303 | + chain) | ||
304 | + (setq chain (nconc (js2-imenu-parent-key-names node) | ||
305 | + (list (js2-prop-node-name key-node)))) | ||
306 | + (push js2-imenu-other-functions-ns chain) | ||
307 | + (js2-record-imenu-entry fn-node chain | ||
308 | + (js2-node-abs-pos key-node))))))) | ||
309 | + | ||
310 | +(defun js2-imenu-record-orphan-assign-node-function (target-node fn-node) | ||
311 | + "Record orphan function FN-NODE assigned to node TARGET." | ||
312 | + (when (or (not js2-imenu-function-map) | ||
313 | + (eq 'skip | ||
314 | + (gethash fn-node js2-imenu-function-map 'skip))) | ||
315 | + (let ((chain (js2-compute-nested-prop-get target-node))) | ||
316 | + (when chain | ||
317 | + (push js2-imenu-other-functions-ns chain) | ||
318 | + (js2-record-imenu-entry fn-node chain (js2-node-abs-pos fn-node)))))) | ||
319 | + | ||
320 | +(defun js2-imenu-record-module-pattern (target init) | ||
321 | + "Recognize and record module pattern use instance. | ||
322 | +INIT must be `js2-call-node'." | ||
323 | + (let ((callt (js2-call-node-target init))) | ||
324 | + ;; Just basic call form: (function() {...})(); | ||
325 | + ;; TODO: Handle variations without duplicating `js2-wrapper-function-p'? | ||
326 | + (when (and (js2-paren-node-p callt) | ||
327 | + (js2-function-node-p (js2-paren-node-expr callt))) | ||
328 | + (let* ((fn (js2-paren-node-expr callt)) | ||
329 | + (blk (js2-function-node-body fn)) | ||
330 | + (ret (car (last (js2-block-node-kids blk))))) | ||
331 | + (when (and (js2-return-node-p ret) | ||
332 | + (js2-object-node-p (js2-return-node-retval ret))) | ||
333 | + ;; TODO: Map function names when revealing module pattern is used. | ||
334 | + (let ((retval (js2-return-node-retval ret)) | ||
335 | + (target-qname (js2-compute-nested-prop-get target))) | ||
336 | + (js2-record-object-literal retval target-qname | ||
337 | + (js2-node-abs-pos retval)) | ||
338 | + (js2-record-imenu-entry fn target-qname | ||
339 | + (js2-node-abs-pos target)))))))) | ||
340 | + | ||
341 | +;;;###autoload | ||
342 | +(define-minor-mode js2-imenu-extras-mode | ||
343 | + "Toggle Imenu support for frameworks and structural patterns." | ||
344 | + :lighter "" | ||
345 | + (if js2-imenu-extras-mode | ||
346 | + (js2-imenu-extras-setup) | ||
347 | + (js2-imenu-extras-remove))) | ||
348 | + | ||
349 | +(provide 'js2-imenu-extras) |
No preview for this file type
1 | +;;; js2-mode-autoloads.el --- automatically extracted autoloads | ||
2 | +;; | ||
3 | +;;; Code: | ||
4 | +(add-to-list 'load-path (or (file-name-directory #$) (car load-path))) | ||
5 | + | ||
6 | +;;;### (autoloads nil "js2-imenu-extras" "js2-imenu-extras.el" (22981 | ||
7 | +;;;;;; 60718 536222 176000)) | ||
8 | +;;; Generated autoloads from js2-imenu-extras.el | ||
9 | + | ||
10 | +(autoload 'js2-imenu-extras-setup "js2-imenu-extras" "\ | ||
11 | + | ||
12 | + | ||
13 | +\(fn)" nil nil) | ||
14 | + | ||
15 | +(autoload 'js2-imenu-extras-mode "js2-imenu-extras" "\ | ||
16 | +Toggle Imenu support for frameworks and structural patterns. | ||
17 | + | ||
18 | +\(fn &optional ARG)" t nil) | ||
19 | + | ||
20 | +;;;*** | ||
21 | + | ||
22 | +;;;### (autoloads nil "js2-mode" "js2-mode.el" (22981 60718 552222 | ||
23 | +;;;;;; 310000)) | ||
24 | +;;; Generated autoloads from js2-mode.el | ||
25 | + | ||
26 | +(autoload 'js2-highlight-unused-variables-mode "js2-mode" "\ | ||
27 | +Toggle highlight of unused variables. | ||
28 | + | ||
29 | +\(fn &optional ARG)" t nil) | ||
30 | + | ||
31 | +(autoload 'js2-minor-mode "js2-mode" "\ | ||
32 | +Minor mode for running js2 as a background linter. | ||
33 | +This allows you to use a different major mode for JavaScript editing, | ||
34 | +such as `js-mode', while retaining the asynchronous error/warning | ||
35 | +highlighting features of `js2-mode'. | ||
36 | + | ||
37 | +\(fn &optional ARG)" t nil) | ||
38 | + | ||
39 | +(autoload 'js2-mode "js2-mode" "\ | ||
40 | +Major mode for editing JavaScript code. | ||
41 | + | ||
42 | +\(fn)" t nil) | ||
43 | + | ||
44 | +(autoload 'js2-jsx-mode "js2-mode" "\ | ||
45 | +Major mode for editing JSX code. | ||
46 | + | ||
47 | +To customize the indentation for this mode, set the SGML offset | ||
48 | +variables (`sgml-basic-offset' et al) locally, like so: | ||
49 | + | ||
50 | + (defun set-jsx-indentation () | ||
51 | + (setq-local sgml-basic-offset js2-basic-offset)) | ||
52 | + (add-hook \\='js2-jsx-mode-hook #\\='set-jsx-indentation) | ||
53 | + | ||
54 | +\(fn)" t nil) | ||
55 | + | ||
56 | +;;;*** | ||
57 | + | ||
58 | +;;;### (autoloads nil nil ("js2-mode-pkg.el" "js2-old-indent.el") | ||
59 | +;;;;;; (22981 60718 574371 921000)) | ||
60 | + | ||
61 | +;;;*** | ||
62 | + | ||
63 | +;; Local Variables: | ||
64 | +;; version-control: never | ||
65 | +;; no-byte-compile: t | ||
66 | +;; no-update-autoloads: t | ||
67 | +;; End: | ||
68 | +;;; js2-mode-autoloads.el ends here |
elpa/js2-mode-20170815.1415/js2-mode-pkg.el
0 → 100644
elpa/js2-mode-20170815.1415/js2-mode.el
0 → 100644
This diff could not be displayed because it is too large.
elpa/js2-mode-20170815.1415/js2-mode.elc
0 → 100644
No preview for this file type
1 | +;;; js2-old-indent.el --- Indentation code kept for compatibility | ||
2 | + | ||
3 | +;; Copyright (C) 2015 Free Software Foundation, Inc. | ||
4 | + | ||
5 | +;; This file is part of GNU Emacs. | ||
6 | + | ||
7 | +;; GNU Emacs 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 | +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. | ||
19 | + | ||
20 | +;;; Commentary: | ||
21 | + | ||
22 | +;; All features of this indentation code have been ported to Emacs's | ||
23 | +;; built-in `js-mode' by now, so we derive from it. An older | ||
24 | +;; commentary follows. | ||
25 | + | ||
26 | +;; This code is kept for Emacs 24.5 and ealier. | ||
27 | + | ||
28 | +;; This indenter is based on Karl Landström's "javascript.el" indenter. | ||
29 | +;; Karl cleverly deduces that the desired indentation level is often a | ||
30 | +;; function of paren/bracket/brace nesting depth, which can be determined | ||
31 | +;; quickly via the built-in `parse-partial-sexp' function. His indenter | ||
32 | +;; then does some equally clever checks to see if we're in the context of a | ||
33 | +;; substatement of a possibly braceless statement keyword such as if, while, | ||
34 | +;; or finally. This approach yields pretty good results. | ||
35 | + | ||
36 | +;; The indenter is often "wrong", however, and needs to be overridden. | ||
37 | +;; The right long-term solution is probably to emulate (or integrate | ||
38 | +;; with) cc-engine, but it's a nontrivial amount of coding. Even when a | ||
39 | +;; parse tree from `js2-parse' is present, which is not true at the | ||
40 | +;; moment the user is typing, computing indentation is still thousands | ||
41 | +;; of lines of code to handle every possible syntactic edge case. | ||
42 | + | ||
43 | +;; In the meantime, the compromise solution is that we offer a "bounce | ||
44 | +;; indenter", configured with `js2-bounce-indent-p', which cycles the | ||
45 | +;; current line indent among various likely guess points. This approach | ||
46 | +;; is far from perfect, but should at least make it slightly easier to | ||
47 | +;; move the line towards its desired indentation when manually | ||
48 | +;; overriding Karl's heuristic nesting guesser. | ||
49 | + | ||
50 | +;; I've made miscellaneous tweaks to Karl's code to handle some Ecma | ||
51 | +;; extensions such as `let' and Array comprehensions. Major kudos to | ||
52 | +;; Karl for coming up with the initial approach, which packs a lot of | ||
53 | +;; punch for so little code. -- Steve | ||
54 | + | ||
55 | +;;; Code: | ||
56 | + | ||
57 | +(require 'sgml-mode) | ||
58 | + | ||
59 | +(defvar js2-language-version) | ||
60 | + | ||
61 | +(declare-function js2-backward-sws "js2-mode") | ||
62 | +(declare-function js2-forward-sws "js2-mode") | ||
63 | +(declare-function js2-same-line "js2-mode") | ||
64 | + | ||
65 | +(defcustom js2-basic-offset (if (and (boundp 'c-basic-offset) | ||
66 | + (numberp c-basic-offset)) | ||
67 | + c-basic-offset | ||
68 | + 4) | ||
69 | + "Number of spaces to indent nested statements. | ||
70 | +Similar to `c-basic-offset'." | ||
71 | + :group 'js2-mode | ||
72 | + :safe 'integerp | ||
73 | + :type 'integer) | ||
74 | + | ||
75 | +(defcustom js2-pretty-multiline-declarations t | ||
76 | + "Non-nil to line up multiline declarations vertically: | ||
77 | + | ||
78 | + var a = 10, | ||
79 | + b = 20, | ||
80 | + c = 30; | ||
81 | + | ||
82 | +If the value is t, and the first assigned value in the | ||
83 | +declaration is a function/array/object literal spanning several | ||
84 | +lines, it won't be indented additionally: | ||
85 | + | ||
86 | + var o = { var bar = 2, | ||
87 | + foo: 3 vs. o = { | ||
88 | + }, foo: 3 | ||
89 | + bar = 2; }; | ||
90 | + | ||
91 | +If the value is `all', it will always be indented additionally: | ||
92 | + | ||
93 | + var o = { | ||
94 | + foo: 3 | ||
95 | + }; | ||
96 | + | ||
97 | + var o = { | ||
98 | + foo: 3 | ||
99 | + }, | ||
100 | + bar = 2; | ||
101 | + | ||
102 | +If the value is `dynamic', it will be indented additionally only | ||
103 | +if the declaration contains more than one variable: | ||
104 | + | ||
105 | + var o = { | ||
106 | + foo: 3 | ||
107 | + }; | ||
108 | + | ||
109 | + var o = { | ||
110 | + foo: 3 | ||
111 | + }, | ||
112 | + bar = 2;" | ||
113 | + :group 'js2-mode | ||
114 | + :safe 'symbolp | ||
115 | + :type 'symbol) | ||
116 | + | ||
117 | +(defcustom js2-indent-switch-body nil | ||
118 | + "When nil, case labels are indented on the same level as the | ||
119 | +containing switch statement. Otherwise, all lines inside | ||
120 | +switch statement body are indented one additional level." | ||
121 | + :type 'boolean | ||
122 | + :safe 'booleanp | ||
123 | + :group 'js2-mode) | ||
124 | + | ||
125 | +(defconst js2-possibly-braceless-keywords-re | ||
126 | + (concat "else[ \t]+if\\|for[ \t]+each\\|" | ||
127 | + (regexp-opt '("catch" "do" "else" "finally" "for" "if" | ||
128 | + "try" "while" "with" "let"))) | ||
129 | + "Regular expression matching keywords that are optionally | ||
130 | +followed by an opening brace.") | ||
131 | + | ||
132 | +(defconst js2-indent-operator-re | ||
133 | + (concat "[-+*/%<>&^|?:.]\\([^-+*/.]\\|$\\)\\|!?=\\|" | ||
134 | + (regexp-opt '("in" "instanceof") 'symbols)) | ||
135 | + "Regular expression matching operators that affect indentation | ||
136 | +of continued expressions.") | ||
137 | + | ||
138 | +(defconst js2-declaration-keyword-re | ||
139 | + (regexp-opt '("var" "let" "const") 'symbols) | ||
140 | + "Regular expression matching variable declaration keywords.") | ||
141 | + | ||
142 | +(defun js2-re-search-forward-inner (regexp &optional bound count) | ||
143 | + "Auxiliary function for `js2-re-search-forward'." | ||
144 | + (let (parse saved-point) | ||
145 | + (while (> count 0) | ||
146 | + (re-search-forward regexp bound) | ||
147 | + (setq parse (if saved-point | ||
148 | + (parse-partial-sexp saved-point (point)) | ||
149 | + (syntax-ppss (point)))) | ||
150 | + (cond ((nth 3 parse) | ||
151 | + (re-search-forward | ||
152 | + (concat "\\(\\=\\|[^\\]\\|^\\)" (string (nth 3 parse))) | ||
153 | + (save-excursion (end-of-line) (point)) t)) | ||
154 | + ((nth 7 parse) | ||
155 | + (forward-line)) | ||
156 | + ((or (nth 4 parse) | ||
157 | + (and (eq (char-before) ?\/) (eq (char-after) ?\*))) | ||
158 | + (re-search-forward "\\*/")) | ||
159 | + (t | ||
160 | + (setq count (1- count)))) | ||
161 | + (setq saved-point (point)))) | ||
162 | + (point)) | ||
163 | + | ||
164 | +(defun js2-re-search-forward (regexp &optional bound noerror count) | ||
165 | + "Search forward but ignore strings and comments. | ||
166 | +Invokes `re-search-forward' but treats the buffer as if strings | ||
167 | +and comments have been removed." | ||
168 | + (let ((saved-point (point))) | ||
169 | + (condition-case err | ||
170 | + (cond ((null count) | ||
171 | + (js2-re-search-forward-inner regexp bound 1)) | ||
172 | + ((< count 0) | ||
173 | + (js2-re-search-backward-inner regexp bound (- count))) | ||
174 | + ((> count 0) | ||
175 | + (js2-re-search-forward-inner regexp bound count))) | ||
176 | + (search-failed | ||
177 | + (goto-char saved-point) | ||
178 | + (unless noerror | ||
179 | + (error (error-message-string err))))))) | ||
180 | + | ||
181 | +(defun js2-re-search-backward-inner (regexp &optional bound count) | ||
182 | + "Auxiliary function for `js2-re-search-backward'." | ||
183 | + (let (parse) | ||
184 | + (while (> count 0) | ||
185 | + (re-search-backward regexp bound) | ||
186 | + (setq parse (syntax-ppss (point))) | ||
187 | + (cond ((nth 3 parse) | ||
188 | + (re-search-backward | ||
189 | + (concat "\\([^\\]\\|^\\)" (string (nth 3 parse))) | ||
190 | + (line-beginning-position) t)) | ||
191 | + ((nth 7 parse) | ||
192 | + (goto-char (nth 8 parse))) | ||
193 | + ((or (nth 4 parse) | ||
194 | + (and (eq (char-before) ?/) (eq (char-after) ?*))) | ||
195 | + (re-search-backward "/\\*")) | ||
196 | + (t | ||
197 | + (setq count (1- count)))))) | ||
198 | + (point)) | ||
199 | + | ||
200 | +(defun js2-re-search-backward (regexp &optional bound noerror count) | ||
201 | + "Search backward but ignore strings and comments. | ||
202 | +Invokes `re-search-backward' but treats the buffer as if strings | ||
203 | +and comments have been removed." | ||
204 | + (let ((saved-point (point))) | ||
205 | + (condition-case err | ||
206 | + (cond ((null count) | ||
207 | + (js2-re-search-backward-inner regexp bound 1)) | ||
208 | + ((< count 0) | ||
209 | + (js2-re-search-forward-inner regexp bound (- count))) | ||
210 | + ((> count 0) | ||
211 | + (js2-re-search-backward-inner regexp bound count))) | ||
212 | + (search-failed | ||
213 | + (goto-char saved-point) | ||
214 | + (unless noerror | ||
215 | + (error (error-message-string err))))))) | ||
216 | + | ||
217 | +(defun js2-looking-at-operator-p () | ||
218 | + "Return non-nil if text after point is a non-comma operator." | ||
219 | + (defvar js2-mode-identifier-re) | ||
220 | + (and (looking-at js2-indent-operator-re) | ||
221 | + (or (not (eq (char-after) ?:)) | ||
222 | + (save-excursion | ||
223 | + (and (js2-re-search-backward "[?:{]\\|\\_<case\\_>" nil t) | ||
224 | + (eq (char-after) ??)))) | ||
225 | + (not (and | ||
226 | + (eq (char-after) ?/) | ||
227 | + (save-excursion | ||
228 | + (eq (nth 3 (syntax-ppss)) ?/)))) | ||
229 | + (not (and | ||
230 | + (eq (char-after) ?*) | ||
231 | + ;; Generator method (possibly using computed property). | ||
232 | + (looking-at (concat "\\* *\\(?:\\[\\|" | ||
233 | + js2-mode-identifier-re | ||
234 | + " *(\\)")) | ||
235 | + (save-excursion | ||
236 | + (js2-backward-sws) | ||
237 | + ;; We might misindent some expressions that would | ||
238 | + ;; return NaN anyway. Shouldn't be a problem. | ||
239 | + (memq (char-before) '(?, ?} ?{))))))) | ||
240 | + | ||
241 | +(defun js2-continued-expression-p () | ||
242 | + "Return non-nil if the current line continues an expression." | ||
243 | + (save-excursion | ||
244 | + (back-to-indentation) | ||
245 | + (if (js2-looking-at-operator-p) | ||
246 | + (or (not (memq (char-after) '(?- ?+))) | ||
247 | + (progn | ||
248 | + (forward-comment (- (point))) | ||
249 | + (not (memq (char-before) '(?, ?\[ ?\())))) | ||
250 | + (forward-comment (- (point))) | ||
251 | + (or (bobp) (backward-char)) | ||
252 | + (when (js2-looking-at-operator-p) | ||
253 | + (backward-char) | ||
254 | + (not (looking-at "\\*\\|\\+\\+\\|--\\|/[/*]")))))) | ||
255 | + | ||
256 | +(defun js2-end-of-do-while-loop-p () | ||
257 | + "Return non-nil if word after point is `while' of a do-while | ||
258 | +statement, else returns nil. A braceless do-while statement | ||
259 | +spanning several lines requires that the start of the loop is | ||
260 | +indented to the same column as the current line." | ||
261 | + (interactive) | ||
262 | + (save-excursion | ||
263 | + (when (looking-at "\\s-*\\_<while\\_>") | ||
264 | + (if (save-excursion | ||
265 | + (skip-chars-backward "[ \t\n]*}") | ||
266 | + (looking-at "[ \t\n]*}")) | ||
267 | + (save-excursion | ||
268 | + (backward-list) (backward-word 1) (looking-at "\\_<do\\_>")) | ||
269 | + (js2-re-search-backward "\\_<do\\_>" (point-at-bol) t) | ||
270 | + (or (looking-at "\\_<do\\_>") | ||
271 | + (let ((saved-indent (current-indentation))) | ||
272 | + (while (and (js2-re-search-backward "^[ \t]*\\_<" nil t) | ||
273 | + (/= (current-indentation) saved-indent))) | ||
274 | + (and (looking-at "[ \t]*\\_<do\\_>") | ||
275 | + (not (js2-re-search-forward | ||
276 | + "\\_<while\\_>" (point-at-eol) t)) | ||
277 | + (= (current-indentation) saved-indent)))))))) | ||
278 | + | ||
279 | +(defun js2-multiline-decl-indentation () | ||
280 | + "Return the declaration indentation column if the current line belongs | ||
281 | +to a multiline declaration statement. See `js2-pretty-multiline-declarations'." | ||
282 | + (let (forward-sexp-function ; use Lisp version | ||
283 | + at-opening-bracket) | ||
284 | + (save-excursion | ||
285 | + (back-to-indentation) | ||
286 | + (when (not (looking-at js2-declaration-keyword-re)) | ||
287 | + (when (looking-at js2-indent-operator-re) | ||
288 | + (goto-char (match-end 0))) ; continued expressions are ok | ||
289 | + (while (and (not at-opening-bracket) | ||
290 | + (not (bobp)) | ||
291 | + (let ((pos (point))) | ||
292 | + (save-excursion | ||
293 | + (js2-backward-sws) | ||
294 | + (or (eq (char-before) ?,) | ||
295 | + (and (not (eq (char-before) ?\;)) | ||
296 | + (prog2 (skip-syntax-backward ".") | ||
297 | + (looking-at js2-indent-operator-re) | ||
298 | + (js2-backward-sws)) | ||
299 | + (not (eq (char-before) ?\;))) | ||
300 | + (js2-same-line pos))))) | ||
301 | + (condition-case _ | ||
302 | + (backward-sexp) | ||
303 | + (scan-error (setq at-opening-bracket t)))) | ||
304 | + (when (looking-at js2-declaration-keyword-re) | ||
305 | + (goto-char (match-end 0)) | ||
306 | + (1+ (current-column))))))) | ||
307 | + | ||
308 | +(defun js2-ctrl-statement-indentation () | ||
309 | + "Return the proper indentation of current line if it is a control statement. | ||
310 | +Returns an indentation if this line starts the body of a control | ||
311 | +statement without braces, else returns nil." | ||
312 | + (let (forward-sexp-function) | ||
313 | + (save-excursion | ||
314 | + (back-to-indentation) | ||
315 | + (when (and (not (js2-same-line (point-min))) | ||
316 | + (not (looking-at "{")) | ||
317 | + (js2-re-search-backward "[[:graph:]]" nil t) | ||
318 | + (not (looking-at "[{([]")) | ||
319 | + (progn | ||
320 | + (forward-char) | ||
321 | + (when (= (char-before) ?\)) | ||
322 | + ;; scan-sexps sometimes throws an error | ||
323 | + (ignore-errors (backward-sexp)) | ||
324 | + (skip-chars-backward " \t" (point-at-bol))) | ||
325 | + (let ((pt (point))) | ||
326 | + (back-to-indentation) | ||
327 | + (when (looking-at "}[ \t]*") | ||
328 | + (goto-char (match-end 0))) | ||
329 | + (and (looking-at js2-possibly-braceless-keywords-re) | ||
330 | + (= (match-end 0) pt) | ||
331 | + (not (js2-end-of-do-while-loop-p)))))) | ||
332 | + (+ (current-indentation) js2-basic-offset))))) | ||
333 | + | ||
334 | +(defun js2-indent-in-array-comp (parse-status) | ||
335 | + "Return non-nil if we think we're in an array comprehension. | ||
336 | +In particular, return the buffer position of the first `for' kwd." | ||
337 | + (let ((bracket (nth 1 parse-status)) | ||
338 | + (end (point))) | ||
339 | + (when bracket | ||
340 | + (save-excursion | ||
341 | + (goto-char bracket) | ||
342 | + (when (looking-at "\\[") | ||
343 | + (forward-char 1) | ||
344 | + (js2-forward-sws) | ||
345 | + (if (looking-at "[[{]") | ||
346 | + (let (forward-sexp-function) ; use Lisp version | ||
347 | + (forward-sexp) ; skip destructuring form | ||
348 | + (js2-forward-sws) | ||
349 | + (if (and (/= (char-after) ?,) ; regular array | ||
350 | + (looking-at "for")) | ||
351 | + (match-beginning 0))) | ||
352 | + ;; to skip arbitrary expressions we need the parser, | ||
353 | + ;; so we'll just guess at it. | ||
354 | + (if (and (> end (point)) ; not empty literal | ||
355 | + (re-search-forward "[^,]]* \\(for\\) " end t) | ||
356 | + ;; not inside comment or string literal | ||
357 | + (let ((state (parse-partial-sexp bracket (point)))) | ||
358 | + (not (or (nth 3 state) (nth 4 state))))) | ||
359 | + (match-beginning 1)))))))) | ||
360 | + | ||
361 | +(defun js2-array-comp-indentation (parse-status for-kwd) | ||
362 | + (if (js2-same-line for-kwd) | ||
363 | + ;; first continuation line | ||
364 | + (save-excursion | ||
365 | + (goto-char (nth 1 parse-status)) | ||
366 | + (forward-char 1) | ||
367 | + (skip-chars-forward " \t") | ||
368 | + (current-column)) | ||
369 | + (save-excursion | ||
370 | + (goto-char for-kwd) | ||
371 | + (current-column)))) | ||
372 | + | ||
373 | +(defun js2-maybe-goto-declaration-keyword-end (bracket) | ||
374 | + "Helper function for `js2-proper-indentation'. | ||
375 | +Depending on the value of `js2-pretty-multiline-declarations', | ||
376 | +move point to the end of a variable declaration keyword so that | ||
377 | +indentation is aligned to that column." | ||
378 | + (cond | ||
379 | + ((eq js2-pretty-multiline-declarations 'all) | ||
380 | + (when (looking-at js2-declaration-keyword-re) | ||
381 | + (goto-char (1+ (match-end 0))))) | ||
382 | + ((eq js2-pretty-multiline-declarations 'dynamic) | ||
383 | + (let (declaration-keyword-end | ||
384 | + at-closing-bracket-p | ||
385 | + comma-p) | ||
386 | + (when (looking-at js2-declaration-keyword-re) | ||
387 | + ;; Preserve the match data lest it somehow be overridden. | ||
388 | + (setq declaration-keyword-end (match-end 0)) | ||
389 | + (save-excursion | ||
390 | + (goto-char bracket) | ||
391 | + (setq at-closing-bracket-p | ||
392 | + ;; Handle scan errors gracefully. | ||
393 | + (condition-case nil | ||
394 | + (progn | ||
395 | + ;; Use the regular `forward-sexp-function' because the | ||
396 | + ;; normal one for this mode uses the AST. | ||
397 | + (let (forward-sexp-function) | ||
398 | + (forward-sexp)) | ||
399 | + t) | ||
400 | + (error nil))) | ||
401 | + (when at-closing-bracket-p | ||
402 | + (js2-forward-sws) | ||
403 | + (setq comma-p (looking-at-p ",")))) | ||
404 | + (when comma-p | ||
405 | + (goto-char (1+ declaration-keyword-end)))))))) | ||
406 | + | ||
407 | +(cl-defun js2-proper-indentation (parse-status) | ||
408 | + "Return the proper indentation for the current line." | ||
409 | + (save-excursion | ||
410 | + (back-to-indentation) | ||
411 | + (when (nth 4 parse-status) | ||
412 | + (cl-return-from js2-proper-indentation (js2--comment-indent parse-status))) | ||
413 | + (let* ((at-closing-bracket (looking-at "[]})]")) | ||
414 | + (same-indent-p (or at-closing-bracket | ||
415 | + (looking-at "\\_<case\\_>[^:]") | ||
416 | + (and (looking-at "\\_<default:") | ||
417 | + (save-excursion | ||
418 | + (js2-backward-sws) | ||
419 | + (not (memq (char-before) '(?, ?{))))))) | ||
420 | + (continued-expr-p (js2-continued-expression-p)) | ||
421 | + (declaration-indent (and js2-pretty-multiline-declarations | ||
422 | + (js2-multiline-decl-indentation))) | ||
423 | + (bracket (nth 1 parse-status)) | ||
424 | + beg indent) | ||
425 | + (cond | ||
426 | + ;; indent array comprehension continuation lines specially | ||
427 | + ((and bracket | ||
428 | + (>= js2-language-version 170) | ||
429 | + (not (js2-same-line bracket)) | ||
430 | + (setq beg (js2-indent-in-array-comp parse-status)) | ||
431 | + (>= (point) (save-excursion | ||
432 | + (goto-char beg) | ||
433 | + (point-at-bol)))) ; at or after first loop? | ||
434 | + (js2-array-comp-indentation parse-status beg)) | ||
435 | + | ||
436 | + ((js2-ctrl-statement-indentation)) | ||
437 | + | ||
438 | + ((and declaration-indent continued-expr-p) | ||
439 | + (+ declaration-indent js2-basic-offset)) | ||
440 | + | ||
441 | + (declaration-indent) | ||
442 | + | ||
443 | + (bracket | ||
444 | + (goto-char bracket) | ||
445 | + (cond | ||
446 | + ((looking-at "[({[][ \t]*\\(/[/*]\\|$\\)") | ||
447 | + (when (save-excursion (skip-chars-backward " \t\n)") | ||
448 | + (looking-at ")")) | ||
449 | + (backward-list)) | ||
450 | + (back-to-indentation) | ||
451 | + (js2-maybe-goto-declaration-keyword-end bracket) | ||
452 | + (setq indent | ||
453 | + (cond (same-indent-p | ||
454 | + (current-column)) | ||
455 | + (continued-expr-p | ||
456 | + (+ (current-column) (* 2 js2-basic-offset))) | ||
457 | + (t | ||
458 | + (+ (current-column) js2-basic-offset)))) | ||
459 | + (if (and js2-indent-switch-body | ||
460 | + (not at-closing-bracket) | ||
461 | + (looking-at "\\_<switch\\_>")) | ||
462 | + (+ indent js2-basic-offset) | ||
463 | + indent)) | ||
464 | + (t | ||
465 | + (unless same-indent-p | ||
466 | + (forward-char) | ||
467 | + (skip-chars-forward " \t")) | ||
468 | + (current-column)))) | ||
469 | + | ||
470 | + (continued-expr-p js2-basic-offset) | ||
471 | + | ||
472 | + (t 0))))) | ||
473 | + | ||
474 | +(defun js2--comment-indent (parse-status) | ||
475 | + "Indentation inside a multi-line block comment continuation line." | ||
476 | + (save-excursion | ||
477 | + (goto-char (nth 8 parse-status)) | ||
478 | + (if (looking-at "/\\*") | ||
479 | + (+ 1 (current-column)) | ||
480 | + 0))) | ||
481 | + | ||
482 | +(defun js2-indent-line (&optional bounce-backwards) | ||
483 | + "Indent the current line as JavaScript source text." | ||
484 | + (interactive) | ||
485 | + (let (parse-status offset | ||
486 | + ;; Don't whine about errors/warnings when we're indenting. | ||
487 | + ;; This has to be set before calling parse-partial-sexp below. | ||
488 | + (inhibit-point-motion-hooks t)) | ||
489 | + (setq parse-status (save-excursion | ||
490 | + (syntax-ppss (point-at-bol))) | ||
491 | + offset (- (point) (save-excursion | ||
492 | + (back-to-indentation) | ||
493 | + (point)))) | ||
494 | + ;; Don't touch multiline strings. | ||
495 | + (unless (nth 3 parse-status) | ||
496 | + (indent-line-to (js2-proper-indentation parse-status)) | ||
497 | + (when (cl-plusp offset) | ||
498 | + (forward-char offset))))) | ||
499 | + | ||
500 | +;;; JSX Indentation | ||
501 | + | ||
502 | +;; The following JSX indentation code is copied basically verbatim from js.el at | ||
503 | +;; 958da7f, except that the prefixes on the functions/variables are changed. | ||
504 | + | ||
505 | +(defsubst js2--jsx-find-before-tag () | ||
506 | + "Find where JSX starts. | ||
507 | + | ||
508 | +Assume JSX appears in the following instances: | ||
509 | +- Inside parentheses, when returned or as the first argument | ||
510 | + to a function, and after a newline | ||
511 | +- When assigned to variables or object properties, but only | ||
512 | + on a single line | ||
513 | +- As the N+1th argument to a function | ||
514 | + | ||
515 | +This is an optimized version of (re-search-backward \"[(,]\n\" | ||
516 | +nil t), except set point to the end of the match. This logic | ||
517 | +executes up to the number of lines in the file, so it should be | ||
518 | +really fast to reduce that impact." | ||
519 | + (let (pos) | ||
520 | + (while (and (> (point) (point-min)) | ||
521 | + (not (progn | ||
522 | + (end-of-line 0) | ||
523 | + (when (or (eq (char-before) 40) ; ( | ||
524 | + (eq (char-before) 44)) ; , | ||
525 | + (setq pos (1- (point)))))))) | ||
526 | + pos)) | ||
527 | + | ||
528 | +(defconst js2--jsx-end-tag-re | ||
529 | + (concat "</" sgml-name-re ">\\|/>") | ||
530 | + "Find the end of a JSX element.") | ||
531 | + | ||
532 | +(defconst js2--jsx-after-tag-re "[),]" | ||
533 | + "Find where JSX ends. | ||
534 | +This complements the assumption of where JSX appears from | ||
535 | +`js--jsx-before-tag-re', which see.") | ||
536 | + | ||
537 | +(defun js2--jsx-indented-element-p () | ||
538 | + "Determine if/how the current line should be indented as JSX. | ||
539 | + | ||
540 | +Return `first' for the first JSXElement on its own line. | ||
541 | +Return `nth' for subsequent lines of the first JSXElement. | ||
542 | +Return `expression' for an embedded JS expression. | ||
543 | +Return `after' for anything after the last JSXElement. | ||
544 | +Return nil for non-JSX lines. | ||
545 | + | ||
546 | +Currently, JSX indentation supports the following styles: | ||
547 | + | ||
548 | +- Single-line elements (indented like normal JS): | ||
549 | + | ||
550 | + var element = <div></div>; | ||
551 | + | ||
552 | +- Multi-line elements (enclosed in parentheses): | ||
553 | + | ||
554 | + function () { | ||
555 | + return ( | ||
556 | + <div> | ||
557 | + <div></div> | ||
558 | + </div> | ||
559 | + ); | ||
560 | + } | ||
561 | + | ||
562 | +- Function arguments: | ||
563 | + | ||
564 | + React.render( | ||
565 | + <div></div>, | ||
566 | + document.querySelector('.root') | ||
567 | + );" | ||
568 | + (let ((current-pos (point)) | ||
569 | + (current-line (line-number-at-pos)) | ||
570 | + last-pos | ||
571 | + before-tag-pos before-tag-line | ||
572 | + tag-start-pos tag-start-line | ||
573 | + tag-end-pos tag-end-line | ||
574 | + after-tag-line | ||
575 | + parens paren type) | ||
576 | + (save-excursion | ||
577 | + (and | ||
578 | + ;; Determine if we're inside a jsx element | ||
579 | + (progn | ||
580 | + (end-of-line) | ||
581 | + (while (and (not tag-start-pos) | ||
582 | + (setq last-pos (js2--jsx-find-before-tag))) | ||
583 | + (while (forward-comment 1)) | ||
584 | + (when (= (char-after) 60) ; < | ||
585 | + (setq before-tag-pos last-pos | ||
586 | + tag-start-pos (point))) | ||
587 | + (goto-char last-pos)) | ||
588 | + tag-start-pos) | ||
589 | + (progn | ||
590 | + (setq before-tag-line (line-number-at-pos before-tag-pos) | ||
591 | + tag-start-line (line-number-at-pos tag-start-pos)) | ||
592 | + (and | ||
593 | + ;; A "before" line which also starts an element begins with js, so | ||
594 | + ;; indent it like js | ||
595 | + (> current-line before-tag-line) | ||
596 | + ;; Only indent the jsx lines like jsx | ||
597 | + (>= current-line tag-start-line))) | ||
598 | + (cond | ||
599 | + ;; Analyze bounds if there are any | ||
600 | + ((progn | ||
601 | + (while (and (not tag-end-pos) | ||
602 | + (setq last-pos (re-search-forward js2--jsx-end-tag-re nil t))) | ||
603 | + (while (forward-comment 1)) | ||
604 | + (when (looking-at js2--jsx-after-tag-re) | ||
605 | + (setq tag-end-pos last-pos))) | ||
606 | + tag-end-pos) | ||
607 | + (setq tag-end-line (line-number-at-pos tag-end-pos) | ||
608 | + after-tag-line (line-number-at-pos after-tag-line)) | ||
609 | + (or (and | ||
610 | + ;; Ensure we're actually within the bounds of the jsx | ||
611 | + (<= current-line tag-end-line) | ||
612 | + ;; An "after" line which does not end an element begins with | ||
613 | + ;; js, so indent it like js | ||
614 | + (<= current-line after-tag-line)) | ||
615 | + (and | ||
616 | + ;; Handle another case where there could be e.g. comments after | ||
617 | + ;; the element | ||
618 | + (> current-line tag-end-line) | ||
619 | + (< current-line after-tag-line) | ||
620 | + (setq type 'after)))) | ||
621 | + ;; They may not be any bounds (yet) | ||
622 | + (t)) | ||
623 | + ;; Check if we're inside an embedded multi-line js expression | ||
624 | + (cond | ||
625 | + ((not type) | ||
626 | + (goto-char current-pos) | ||
627 | + (end-of-line) | ||
628 | + (setq parens (nth 9 (syntax-ppss))) | ||
629 | + (while (and parens (not type)) | ||
630 | + (setq paren (car parens)) | ||
631 | + (cond | ||
632 | + ((and (>= paren tag-start-pos) | ||
633 | + ;; Curly bracket indicates the start of an embedded expression | ||
634 | + (= (char-after paren) 123) ; { | ||
635 | + ;; The first line of the expression is indented like sgml | ||
636 | + (> current-line (line-number-at-pos paren)) | ||
637 | + ;; Check if within a closing curly bracket (if any) | ||
638 | + ;; (exclusive, as the closing bracket is indented like sgml) | ||
639 | + (cond | ||
640 | + ((progn | ||
641 | + (goto-char paren) | ||
642 | + (ignore-errors (let (forward-sexp-function) | ||
643 | + (forward-sexp)))) | ||
644 | + (< current-line (line-number-at-pos))) | ||
645 | + (t))) | ||
646 | + ;; Indicate this guy will be indented specially | ||
647 | + (setq type 'expression)) | ||
648 | + (t (setq parens (cdr parens))))) | ||
649 | + t) | ||
650 | + (t)) | ||
651 | + (cond | ||
652 | + (type) | ||
653 | + ;; Indent the first jsx thing like js so we can indent future jsx things | ||
654 | + ;; like sgml relative to the first thing | ||
655 | + ((= current-line tag-start-line) 'first) | ||
656 | + ('nth)))))) | ||
657 | + | ||
658 | +(defmacro js2--as-sgml (&rest body) | ||
659 | + "Execute BODY as if in sgml-mode." | ||
660 | + `(with-syntax-table sgml-mode-syntax-table | ||
661 | + (let (forward-sexp-function | ||
662 | + parse-sexp-lookup-properties) | ||
663 | + ,@body))) | ||
664 | + | ||
665 | +(defun js2--expression-in-sgml-indent-line () | ||
666 | + "Indent the current line as JavaScript or SGML (whichever is farther)." | ||
667 | + (let* (indent-col | ||
668 | + (savep (point)) | ||
669 | + ;; Don't whine about errors/warnings when we're indenting. | ||
670 | + ;; This has to be set before calling parse-partial-sexp below. | ||
671 | + (inhibit-point-motion-hooks t) | ||
672 | + (parse-status (save-excursion | ||
673 | + (syntax-ppss (point-at-bol))))) | ||
674 | + ;; Don't touch multiline strings. | ||
675 | + (unless (nth 3 parse-status) | ||
676 | + (setq indent-col (save-excursion | ||
677 | + (back-to-indentation) | ||
678 | + (if (>= (point) savep) (setq savep nil)) | ||
679 | + (js2--as-sgml (sgml-calculate-indent)))) | ||
680 | + (if (null indent-col) | ||
681 | + 'noindent | ||
682 | + ;; Use whichever indentation column is greater, such that the sgml | ||
683 | + ;; column is effectively a minimum | ||
684 | + (setq indent-col (max (js2-proper-indentation parse-status) | ||
685 | + (+ indent-col js2-basic-offset))) | ||
686 | + (if savep | ||
687 | + (save-excursion (indent-line-to indent-col)) | ||
688 | + (indent-line-to indent-col)))))) | ||
689 | + | ||
690 | +(defun js2-jsx-indent-line () | ||
691 | + "Indent the current line as JSX (with SGML offsets). | ||
692 | +i.e., customize JSX element indentation with `sgml-basic-offset' | ||
693 | +et al." | ||
694 | + (interactive) | ||
695 | + (let ((indentation-type (js2--jsx-indented-element-p))) | ||
696 | + (cond | ||
697 | + ((eq indentation-type 'expression) | ||
698 | + (js2--expression-in-sgml-indent-line)) | ||
699 | + ((or (eq indentation-type 'first) | ||
700 | + (eq indentation-type 'after)) | ||
701 | + ;; Don't treat this first thing as a continued expression (often a "<" or | ||
702 | + ;; ">" causes this misinterpretation) | ||
703 | + (cl-letf (((symbol-function #'js2-continued-expression-p) 'ignore)) | ||
704 | + (js2-indent-line))) | ||
705 | + ((eq indentation-type 'nth) | ||
706 | + (js2--as-sgml (sgml-indent-line))) | ||
707 | + (t (js2-indent-line))))) | ||
708 | + | ||
709 | +(provide 'js2-old-indent) | ||
710 | + | ||
711 | +;;; js2-old-indent.el ends here |
No preview for this file type
1 | +;;; rjsx-mode-autoloads.el --- automatically extracted autoloads | ||
2 | +;; | ||
3 | +;;; Code: | ||
4 | +(add-to-list 'load-path (or (file-name-directory #$) (car load-path))) | ||
5 | + | ||
6 | +;;;### (autoloads nil "rjsx-mode" "rjsx-mode.el" (22981 60721 372245 | ||
7 | +;;;;;; 990000)) | ||
8 | +;;; Generated autoloads from rjsx-mode.el | ||
9 | + | ||
10 | +(autoload 'rjsx-mode "rjsx-mode" "\ | ||
11 | +Major mode for editing JSX files. | ||
12 | + | ||
13 | +\(fn)" t nil) | ||
14 | + | ||
15 | +(add-to-list 'auto-mode-alist '("\\.jsx\\'" . rjsx-mode)) | ||
16 | + | ||
17 | +;;;*** | ||
18 | + | ||
19 | +;; Local Variables: | ||
20 | +;; version-control: never | ||
21 | +;; no-byte-compile: t | ||
22 | +;; no-update-autoloads: t | ||
23 | +;; End: | ||
24 | +;;; rjsx-mode-autoloads.el ends here |
elpa/rjsx-mode-20170808.634/rjsx-mode-pkg.el
0 → 100644
1 | +(define-package "rjsx-mode" "20170808.634" "Real support for JSX" '((emacs "24.4") (js2-mode "20170504")) :commit "4a24c86a1873289538134fe431e544fa3e12e788" :url "https://github.com/felipeochoa/rjsx-mode/" :keywords '("languages")) |
No preview for this file type
elpa/rjsx-mode-20170808.634/rjsx-mode.el
0 → 100644
1 | +;;; rjsx-mode.el --- Real support for JSX -*- lexical-binding: t -*- | ||
2 | + | ||
3 | +;; Copyright (C) 2016 Felipe Ochoa | ||
4 | + | ||
5 | +;; Author: Felipe Ochoa <felipe@fov.space> | ||
6 | +;; URL: https://github.com/felipeochoa/rjsx-mode/ | ||
7 | +;; Package-Version: 20170808.634 | ||
8 | +;; Package-Requires: ((emacs "24.4") (js2-mode "20170504")) | ||
9 | +;; Version: 1.1 | ||
10 | +;; Keywords: languages | ||
11 | + | ||
12 | +;;; Commentary: | ||
13 | +;; Defines a major mode `rjsx-mode' based on `js2-mode' for editing | ||
14 | +;; JSX files. `rjsx-mode' extends the parser in `js2-mode' to support | ||
15 | +;; the full JSX syntax. This means you get all of the `js2' features | ||
16 | +;; plus proper syntax checking and highlighting of JSX code blocks. | ||
17 | +;; | ||
18 | +;; Some features that this mode adds to js2: | ||
19 | +;; | ||
20 | +;; - Highlighting JSX tag names and attributes (using the rjsx-tag and | ||
21 | +;; rjsx-attr faces) | ||
22 | +;; - Highlight undeclared JSX components | ||
23 | +;; - Parsing the spread operator {...otherProps} | ||
24 | +;; - Parsing && and || in child expressions {cond && <BigComponent/>} | ||
25 | +;; - Parsing ternary expressions {toggle ? <ToggleOn /> : <ToggleOff />} | ||
26 | +;; | ||
27 | +;; Additionally, since rjsx-mode extends the js2 AST, utilities using | ||
28 | +;; the parse tree gain access to the JSX structure. | ||
29 | + | ||
30 | +;;; Code: | ||
31 | + | ||
32 | +;;;; Basic mode definitions | ||
33 | + | ||
34 | +(require 'cl-lib) | ||
35 | +(require 'js2-mode) | ||
36 | + | ||
37 | +(defgroup rjsx-mode nil | ||
38 | + "Support for JSX." | ||
39 | + :group 'js2-mode) | ||
40 | + | ||
41 | +;;;###autoload | ||
42 | +(define-derived-mode rjsx-mode js2-jsx-mode "RJSX" | ||
43 | + "Major mode for editing JSX files." | ||
44 | + :lighter ":RJSX" | ||
45 | + :group 'rjsx-mode) | ||
46 | + | ||
47 | +;;;###autoload | ||
48 | +(add-to-list 'auto-mode-alist '("\\.jsx\\'" . rjsx-mode)) | ||
49 | + | ||
50 | +(defun rjsx-parse-xml-initializer (orig-fun) | ||
51 | + "Dispatch the xml parser based on variable `rjsx-mode' being active or not. | ||
52 | +This function is used to advise `js2-parse-xml-initializer' (ORIG-FUN) using | ||
53 | +the `:around' combinator. JS2-PARSER is the original XML parser." | ||
54 | + (if (eq major-mode 'rjsx-mode) | ||
55 | + (rjsx-parse-top-xml) | ||
56 | + (apply orig-fun nil))) | ||
57 | + | ||
58 | +(advice-add 'js2-parse-xml-initializer :around #'rjsx-parse-xml-initializer) | ||
59 | + | ||
60 | +(defun rjsx-unadvice-js2 () | ||
61 | + "Remove the rjsx advice on the js2 parser. This will cause rjsx to stop working globally." | ||
62 | + (advice-remove 'js2-parse-xml-initializer #'rjsx-parse-xml-initializer)) | ||
63 | + | ||
64 | + | ||
65 | +(defface rjsx-tag | ||
66 | + '((t . (:inherit font-lock-function-name-face))) | ||
67 | + "`rjsx-mode' face used to highlight JSX tag names." | ||
68 | + :group 'rjsx-mode) | ||
69 | + | ||
70 | +(defface rjsx-attr | ||
71 | + '((t . (:inherit font-lock-variable-name-face))) | ||
72 | + "`rjsx-mode' face used to highlight JSX attribute names." | ||
73 | + :group 'rjsx-mode) | ||
74 | + | ||
75 | +(defface rjsx-text | ||
76 | + '((t . (:inherit font-lock-string-face))) | ||
77 | + "`rjsx-mode' face used to highlight JSX text." | ||
78 | + :group 'rjsx-mode) | ||
79 | + | ||
80 | + | ||
81 | +;;;; Parser constants struct definitions | ||
82 | + | ||
83 | +;; Token types for XML nodes. We need to re-use some unused values to | ||
84 | +;; not mess up the vectors that js2 has set up | ||
85 | +(defvar rjsx-JSX js2-ENUM_INIT_KEYS) | ||
86 | +(defvar rjsx-JSX-CLOSE js2-ENUM_INIT_VALUES) | ||
87 | +(defvar rjsx-JSX-IDENT js2-ENUM_INIT_ARRAY) | ||
88 | +(defvar rjsx-JSX-MEMBER js2-ENUM_NEXT) | ||
89 | +(defvar rjsx-JSX-ATTR js2-ENUM_ID) | ||
90 | +(defvar rjsx-JSX-SPREAD js2-REF_NS_MEMBER) | ||
91 | +(defvar rjsx-JSX-TEXT js2-ESCXMLTEXT) | ||
92 | +(defvar rjsx-JSX-EXPRESSION js2-ESCXMLATTR) | ||
93 | + | ||
94 | +(dolist (sym '(rjsx-JSX rjsx-JSX-CLOSE rjsx-JSX-IDENT rjsx-JSX-MEMBER rjsx-JSX-ATTR | ||
95 | + rjsx-JSX-SPREAD rjsx-JSX-TEXT rjsx-JSX-EXPRESSION)) | ||
96 | + (aset js2-token-names (symbol-value sym) (downcase (substring (symbol-name sym) 5))) | ||
97 | + (puthash sym (symbol-value sym) js2-token-codes)) | ||
98 | + | ||
99 | +(js2-msg "msg.bad.jsx.ident" "invalid JSX identifier") | ||
100 | +(js2-msg "msg.invalid.jsx.string" "invalid JSX string (cannot contain delimiter in string body)") | ||
101 | +(js2-msg "msg.mismatched.close.tag" "mismatched closing JSX tag; expected `%s'") | ||
102 | +(js2-msg "msg.no.gt.in.opener" "missing `>' in opening tag") | ||
103 | +(js2-msg "msg.no.gt.in.closer" "missing `>' in closing tag") | ||
104 | +(js2-msg "msg.no.gt.after.slash" "missing `>' after `/' in self-closing tag") | ||
105 | +(js2-msg "msg.no.rc.after.spread" "missing `}' after spread-prop") | ||
106 | +(js2-msg "msg.no.value.after.jsx.prop" "missing value after prop `%s'") | ||
107 | +(js2-msg "msg.no.dots.in.prop.spread" "missing `...' in spread prop") | ||
108 | +(js2-msg "msg.no.rc.after.expr" "missing `}' after expression") | ||
109 | +(js2-msg "msg.empty.expr" "empty `{}' expression") | ||
110 | + | ||
111 | + | ||
112 | +(cl-defstruct (rjsx-node | ||
113 | + (:include js2-node (type rjsx-JSX)) | ||
114 | + (:constructor nil) | ||
115 | + (:constructor make-rjsx-node | ||
116 | + (&key (pos (js2-current-token-beg)) | ||
117 | + len | ||
118 | + name | ||
119 | + rjsx-props | ||
120 | + kids))) | ||
121 | + name ; AST node containing the parsed xml name | ||
122 | + rjsx-props ; linked list of AST nodes (both attributes and spreads) | ||
123 | + kids ; linked list of child xml nodes | ||
124 | + closing-tag) ; AST node with the tag closer | ||
125 | + | ||
126 | + | ||
127 | +(js2--struct-put 'rjsx-node 'js2-visitor 'rjsx-node-visit) | ||
128 | +(js2--struct-put 'rjsx-node 'js2-printer 'rjsx-node-print) | ||
129 | +(defun rjsx-node-visit (ast callback) | ||
130 | + "Visit the `rjsx-node' children of AST, invoking CALLBACK on them." | ||
131 | + (js2-visit-ast (rjsx-node-name ast) callback) | ||
132 | + (dolist (prop (rjsx-node-rjsx-props ast)) | ||
133 | + (js2-visit-ast prop callback)) | ||
134 | + (dolist (prop (rjsx-node-kids ast)) | ||
135 | + (js2-visit-ast prop callback)) | ||
136 | + (when (rjsx-node-closing-tag ast) | ||
137 | + (js2-visit-ast (rjsx-node-closing-tag ast) callback))) | ||
138 | + | ||
139 | +(defun rjsx-node-print (node indent-level) | ||
140 | + "Print the `rjsx-node' NODE at indent level INDENT-LEVEL." | ||
141 | + (insert (js2-make-pad indent-level) "<") | ||
142 | + (js2-print-ast (rjsx-node-name node) 0) | ||
143 | + (dolist (attr (rjsx-node-rjsx-props node)) | ||
144 | + (insert " ") | ||
145 | + (js2-print-ast attr 0)) | ||
146 | + (let ((closer (rjsx-node-closing-tag node))) | ||
147 | + (if (null closer) | ||
148 | + (insert "/>") | ||
149 | + (insert ">") | ||
150 | + (dolist (child (rjsx-node-kids node)) | ||
151 | + (js2-print-ast child 0)) | ||
152 | + (js2-print-ast closer indent-level)))) | ||
153 | + | ||
154 | +(defun rjsx-node-opening-tag-name (node) | ||
155 | + "Return a string with NODE's opening tag including any namespace and member operations." | ||
156 | + (let ((name-n (rjsx-node-name node))) | ||
157 | + (cond | ||
158 | + ((rjsx-member-p name-n) (rjsx-member-full-name name-n)) | ||
159 | + ((rjsx-identifier-p name-n) (rjsx-identifier-full-name name-n)) | ||
160 | + ;; Otherwise it's either nil or an error. Either way, no name :( | ||
161 | + (t "")))) | ||
162 | + | ||
163 | +(defun rjsx-node-push-prop (n rjsx-prop) | ||
164 | + "Extend rjsx-node N's rjsx-props with js2-node RJSX-PROP. | ||
165 | +Sets JSX-PROPS's parent to N." | ||
166 | + (let ((rjsx-props (rjsx-node-rjsx-props n))) | ||
167 | + (if rjsx-props | ||
168 | + (setcdr rjsx-props (nconc (cdr rjsx-props) (list rjsx-prop))) | ||
169 | + (setf (rjsx-node-rjsx-props n) (list rjsx-prop)))) | ||
170 | + (js2-node-add-children n rjsx-prop)) | ||
171 | + | ||
172 | +(defun rjsx-node-push-child (n kid) | ||
173 | + "Extend rjsx-node N's children with js2-node KID. | ||
174 | +Sets KID's parent to N." | ||
175 | + (let ((kids (rjsx-node-kids n))) | ||
176 | + (if kids | ||
177 | + (setcdr kids (nconc (cdr kids) (list kid))) | ||
178 | + (setf (rjsx-node-kids n) (list kid)))) | ||
179 | + (js2-node-add-children n kid)) | ||
180 | + | ||
181 | + | ||
182 | +(cl-defstruct (rjsx-closing-tag | ||
183 | + (:include js2-node (type rjsx-JSX-CLOSE)) | ||
184 | + (:constructor nil) | ||
185 | + (:constructor make-rjsx-closing-tag (&key pos len name))) | ||
186 | + name) ; A rjsx-identifier or rjsx-member node | ||
187 | + | ||
188 | +(js2--struct-put 'rjsx-closing-tag 'js2-visitor 'rjsx-closing-tag-visit) | ||
189 | +(js2--struct-put 'rjsx-closing-tag 'js2-printer 'rjsx-closing-tag-print) | ||
190 | + | ||
191 | +(defun rjsx-closing-tag-visit (ast callback) | ||
192 | + "Visit the `rjsx-closing-tag' children of AST, invoking CALLBACK on them." | ||
193 | + (js2-visit-ast (rjsx-closing-tag-name ast) callback)) | ||
194 | + | ||
195 | +(defun rjsx-closing-tag-print (node indent-level) | ||
196 | + "Print the `rjsx-closing-tag' NODE at INDENT-LEVEL." | ||
197 | + (insert (js2-make-pad indent-level) "</" (rjsx-closing-tag-full-name node) ">")) | ||
198 | + | ||
199 | +(defun rjsx-closing-tag-full-name (n) | ||
200 | + "Return the string with N's fully-namespaced name, or just name if it's not namespaced." | ||
201 | + (let ((child (rjsx-closing-tag-name n))) | ||
202 | + (cond | ||
203 | + ((rjsx-member-p child) (rjsx-member-full-name child)) | ||
204 | + ((rjsx-identifier-p child) (rjsx-identifier-full-name child)) | ||
205 | + (t "")))) | ||
206 | + | ||
207 | +(cl-defstruct (rjsx-identifier | ||
208 | + (:include js2-node (type rjsx-JSX-IDENT)) | ||
209 | + (:constructor nil) | ||
210 | + (:constructor make-rjsx-identifier (&key (pos (js2-current-token-beg)) | ||
211 | + len namespace name))) | ||
212 | + (namespace nil) | ||
213 | + name) ; js2-name-node | ||
214 | + | ||
215 | +(js2--struct-put 'rjsx-identifier 'js2-visitor 'js2-visit-none) | ||
216 | +(js2--struct-put 'rjsx-identifier 'js2-printer 'rjsx-identifier-print) | ||
217 | + | ||
218 | +(defun rjsx-identifier-print (node indent-level) | ||
219 | + "Print the `rjsx-identifier' NODE at INDENT-LEVEL." | ||
220 | + (insert (js2-make-pad indent-level) (rjsx-identifier-full-name node))) | ||
221 | + | ||
222 | +(defun rjsx-identifier-full-name (n) | ||
223 | + "Return the string with N's fully-namespaced name, or just name if it's not namespaced." | ||
224 | + (if (rjsx-identifier-namespace n) | ||
225 | + (format "%s:%s" (rjsx-identifier-namespace n) (js2-name-node-name (rjsx-identifier-name n))) | ||
226 | + (js2-name-node-name (rjsx-identifier-name n)))) | ||
227 | + | ||
228 | +(cl-defstruct (rjsx-member | ||
229 | + (:include js2-node (type rjsx-JSX-MEMBER)) | ||
230 | + (:constructor nil) | ||
231 | + (:constructor make-rjsx-member (&key pos len dots-pos idents))) | ||
232 | + dots-pos ; List of positions of each dot | ||
233 | + idents) ; List of rjsx-identifier nodes | ||
234 | + | ||
235 | +(js2--struct-put 'rjsx-member 'js2-visitor 'js2-visit-none) | ||
236 | +(js2--struct-put 'rjsx-member 'js2-printer 'rjsx-member-print) | ||
237 | + | ||
238 | +(defun rjsx-member-print (node indent-level) | ||
239 | + "Print the `rjsx-member' NODE at INDENT-LEVEL." | ||
240 | + (insert (js2-make-pad indent-level) (rjsx-member-full-name node))) | ||
241 | + | ||
242 | +(defun rjsx-member-full-name (n) | ||
243 | + "Return the string with N's combined names together." | ||
244 | + (mapconcat 'rjsx-identifier-full-name (rjsx-member-idents n) ".")) | ||
245 | + | ||
246 | +(cl-defstruct (rjsx-attr | ||
247 | + (:include js2-node (type rjsx-JSX-ATTR)) | ||
248 | + (:constructor nil) | ||
249 | + (:constructor make-rjsx-attr (&key (pos (js2-current-token-beg)) | ||
250 | + len name value))) | ||
251 | + name ; a rjsx-identifier | ||
252 | + value) ; a js2-expression | ||
253 | + | ||
254 | +(js2--struct-put 'rjsx-attr 'js2-visitor 'rjsx-attr-visit) | ||
255 | +(js2--struct-put 'rjsx-attr 'js2-printer 'rjsx-attr-print) | ||
256 | + | ||
257 | +(defun rjsx-attr-visit (ast callback) | ||
258 | + "Visit the `rjsx-attr' children of AST, invoking CALLBACK on them." | ||
259 | + (js2-visit-ast (rjsx-attr-name ast) callback) | ||
260 | + (js2-visit-ast (rjsx-attr-value ast) callback)) | ||
261 | + | ||
262 | +(defun rjsx-attr-print (node indent-level) | ||
263 | + "Print the `rjsx-attr' NODE at INDENT-LEVEL." | ||
264 | + (js2-print-ast (rjsx-attr-name node) indent-level) | ||
265 | + (unless (js2-empty-expr-node-p (rjsx-attr-value node)) | ||
266 | + (insert "=") | ||
267 | + (js2-print-ast (rjsx-attr-value node) 0))) | ||
268 | + | ||
269 | +(cl-defstruct (rjsx-spread | ||
270 | + (:include js2-node (type rjsx-JSX-SPREAD)) | ||
271 | + (:constructor nil) | ||
272 | + (:constructor make-rjsx-spread (&key pos len expr))) | ||
273 | + expr) ; a js2-expression | ||
274 | + | ||
275 | +(js2--struct-put 'rjsx-spread 'js2-visitor 'rjsx-spread-visit) | ||
276 | +(js2--struct-put 'rjsx-spread 'js2-printer 'rjsx-spread-print) | ||
277 | + | ||
278 | +(defun rjsx-spread-visit (ast callback) | ||
279 | + "Visit the `rjsx-spread' children of AST, invoking CALLBACK on them." | ||
280 | + (js2-visit-ast (rjsx-spread-expr ast) callback)) | ||
281 | + | ||
282 | +(defun rjsx-spread-print (node indent-level) | ||
283 | + "Print the `rjsx-spread' NODE at INDENT-LEVEL." | ||
284 | + (insert (js2-make-pad indent-level) "{...") | ||
285 | + (js2-print-ast (rjsx-spread-expr node) 0) | ||
286 | + (insert "}")) | ||
287 | + | ||
288 | +(cl-defstruct (rjsx-wrapped-expr | ||
289 | + (:include js2-node (type rjsx-JSX-TEXT)) | ||
290 | + (:constructor nil) | ||
291 | + (:constructor make-rjsx-wrapped-expr (&key pos len child))) | ||
292 | + child) | ||
293 | + | ||
294 | +(js2--struct-put 'rjsx-wrapped-expr 'js2-visitor 'rjsx-wrapped-expr-visit) | ||
295 | +(js2--struct-put 'rjsx-wrapped-expr 'js2-printer 'rjsx-wrapped-expr-print) | ||
296 | + | ||
297 | +(defun rjsx-wrapped-expr-visit (ast callback) | ||
298 | + "Visit the `rjsx-wrapped-expr' child of AST, invoking CALLBACK on them." | ||
299 | + (js2-visit-ast (rjsx-wrapped-expr-child ast) callback)) | ||
300 | + | ||
301 | +(defun rjsx-wrapped-expr-print (node indent-level) | ||
302 | + "Print the `rjsx-wrapped-expr' NODE at INDENT-LEVEL." | ||
303 | + (insert (js2-make-pad indent-level) "{") | ||
304 | + (js2-print-ast (rjsx-wrapped-expr-child node) indent-level) | ||
305 | + (insert "}")) | ||
306 | + | ||
307 | +(cl-defstruct (rjsx-text | ||
308 | + (:include js2-node (type rjsx-JSX-TEXT)) | ||
309 | + (:constructor nil) | ||
310 | + (:constructor make-rjsx-text (&key (pos (js2-current-token-beg)) | ||
311 | + (len (js2-current-token-len)) | ||
312 | + value))) | ||
313 | + value) ; a string | ||
314 | + | ||
315 | +(js2--struct-put 'rjsx-text 'js2-visitor 'js2-visit-none) | ||
316 | +(js2--struct-put 'rjsx-text 'js2-printer 'rjsx-text-print) | ||
317 | + | ||
318 | +(defun rjsx-text-print (node _indent-level) | ||
319 | + "Print the `rjsx-text' NODE at INDENT-LEVEL." | ||
320 | + ;; Text nodes include whitespace | ||
321 | + (insert (rjsx-text-value node))) | ||
322 | + | ||
323 | + | ||
324 | +;;;; Recursive descent parsing | ||
325 | +(defvar rjsx-print-debug-message nil "If t will print out debug messages.") | ||
326 | +;(setq rjsx-print-debug-message t) | ||
327 | +(defmacro rjsx-maybe-message (&rest args) | ||
328 | + "If debug is enabled, call `message' with ARGS." | ||
329 | + `(when rjsx-print-debug-message | ||
330 | + (message ,@args))) | ||
331 | + | ||
332 | + | ||
333 | +(js2-deflocal rjsx-in-xml nil "Variable used to track which xml parsing function is the outermost one.") | ||
334 | + | ||
335 | +(defun rjsx-parse-top-xml () | ||
336 | + "Parse a top level XML fragment. | ||
337 | +This is the entry point when ‘js2-parse-unary-expr’ finds a '<' character" | ||
338 | + (rjsx-maybe-message "Parsing a new xml fragment%s" (if rjsx-in-xml ", recursively" "")) | ||
339 | + ;; If there are imbalanced tags, we just need to bail out to the | ||
340 | + ;; topmost JSX parser and let js2 handle the EOF. Our custom scanner | ||
341 | + ;; will throw `t' if it finds the EOF, which it ordinarily wouldn't | ||
342 | + (let (pn) | ||
343 | + (when (catch 'rjsx-eof-while-parsing | ||
344 | + (let ((rjsx-in-xml t)) ;; We use dynamic scope to handle xml > expr > xml nestings | ||
345 | + (setq pn (rjsx-parse-xml))) | ||
346 | + nil) | ||
347 | + (rjsx-maybe-message "Caught a signal. Rethrowing?: `%s'" rjsx-in-xml) | ||
348 | + (if rjsx-in-xml | ||
349 | + (throw 'rjsx-eof-while-parsing t) | ||
350 | + ;; We subtract 1 since js2 sets the cursor the the point after point-max | ||
351 | + (setq pn (make-js2-error-node :len (1- (js2-current-token-len)))) | ||
352 | + (js2-report-error "msg.syntax" nil (js2-node-pos pn) (js2-node-len pn)))) | ||
353 | + (rjsx-maybe-message "Returning from top xml function: %s" pn) | ||
354 | + pn)) | ||
355 | + | ||
356 | +(defun rjsx-parse-xml () | ||
357 | + "Parse a complete xml node from start to end tag." | ||
358 | + (let ((pn (make-rjsx-node)) self-closing name-n name-str child child-name-str) | ||
359 | + (rjsx-maybe-message "Starting rjsx-parse-xml after <") | ||
360 | + (if (setq child (rjsx-parse-empty-tag)) | ||
361 | + child | ||
362 | + (setf (rjsx-node-name pn) (setq name-n (rjsx-parse-member-or-ns 'rjsx-tag))) | ||
363 | + (if (js2-error-node-p name-n) | ||
364 | + (progn (rjsx-maybe-message "could not parse tag name") | ||
365 | + (make-js2-error-node :pos (js2-node-pos pn) :len (1+ (js2-node-len name-n)))) | ||
366 | + (js2-node-add-children pn name-n) | ||
367 | + (setq name-str (if (rjsx-member-p name-n) (rjsx-member-full-name name-n) | ||
368 | + (rjsx-identifier-full-name name-n))) | ||
369 | + (if js2-highlight-external-variables | ||
370 | + (let ((name-node (rjsx-identifier-name | ||
371 | + (if (rjsx-member-p name-n) | ||
372 | + (car (rjsx-member-idents name-n)) | ||
373 | + name-n))) | ||
374 | + (case-fold-search nil)) | ||
375 | + (when (string-match-p "^[[:upper:]]" (js2-name-node-name name-node)) | ||
376 | + (js2-record-name-node name-node)))) | ||
377 | + (rjsx-maybe-message "cleared tag name: '%s'" name-str) | ||
378 | + ;; Now parse the attributes | ||
379 | + (rjsx-parse-attributes pn) | ||
380 | + (rjsx-maybe-message "cleared attributes") | ||
381 | + ;; Now parse either a self closing tag or the end of the opening tag | ||
382 | + (rjsx-maybe-message "next type: `%s'" (js2-peek-token)) | ||
383 | + (if (setq self-closing (js2-match-token js2-DIV)) | ||
384 | + (progn | ||
385 | + (js2-record-text-property (js2-current-token-beg) (js2-current-token-end) | ||
386 | + 'rjsx-class 'self-closing-slash) | ||
387 | + ;; TODO: How do we un-mark old slashes? | ||
388 | + (js2-must-match js2-GT "msg.no.gt.after.slash" | ||
389 | + (js2-node-pos pn) (- (js2-current-token-end) (js2-node-pos pn)))) | ||
390 | + (js2-must-match js2-GT "msg.no.gt.in.opener" (js2-node-pos pn) (js2-node-len pn))) | ||
391 | + (rjsx-maybe-message "cleared opener closer, self-closing: %s" self-closing) | ||
392 | + (if self-closing | ||
393 | + (setf (js2-node-len pn) (- (js2-current-token-end) (js2-node-pos pn))) | ||
394 | + (while (not (rjsx-closing-tag-p (setq child (rjsx-parse-child)))) | ||
395 | + ;; rjsx-parse-child calls our scanner, which always moves | ||
396 | + ;; forward at least one character. If it hits EOF, it | ||
397 | + ;; signals to our caller, so we don't have to worry about infinite loops here | ||
398 | + (rjsx-maybe-message "parsed child") | ||
399 | + (rjsx-node-push-child pn child) | ||
400 | + (if (= 0 (js2-node-len child)) ; TODO: Does this ever happen? | ||
401 | + (js2-get-token))) | ||
402 | + (setq child-name-str (rjsx-closing-tag-full-name child)) | ||
403 | + (unless (string= name-str child-name-str) | ||
404 | + (js2-report-error "msg.mismatched.close.tag" name-str (js2-node-pos child) (js2-node-len child))) | ||
405 | + (rjsx-maybe-message "cleared children for `%s'" name-str) | ||
406 | + (js2-node-add-children pn child) | ||
407 | + (setf (rjsx-node-closing-tag pn) child)) | ||
408 | + (rjsx-maybe-message "Returning completed XML node") | ||
409 | + (setf (js2-node-len pn) (- (js2-current-token-end) (js2-node-pos pn))) | ||
410 | + pn)))) | ||
411 | + | ||
412 | +(defun rjsx-parse-empty-tag () | ||
413 | + "Check if we are in an empty tag of the form `</>' and consume it if so. | ||
414 | +Returns a `js2-error-node' if we are in one or nil if not." | ||
415 | + (let ((beg (js2-current-token-beg))) | ||
416 | + (when (js2-match-token js2-DIV) | ||
417 | + (if (js2-match-token js2-GT) | ||
418 | + (progn ; We're in a </> block, likely created by us in `rjsx-electric-lt' | ||
419 | + ;; We only highlight the < to reduce the visual impact | ||
420 | + (js2-report-error "msg.syntax" nil beg 1) | ||
421 | + (make-js2-error-node :pos beg :len (- (js2-current-token-end) beg))) | ||
422 | + ;; TODO: This is probably an unmatched closing tag. We should | ||
423 | + ;; consume it, mark it an error, and move on | ||
424 | + (js2-unget-token) | ||
425 | + nil)))) | ||
426 | + | ||
427 | +(defun rjsx-parse-attributes (parent) | ||
428 | + "Parse all attributes, including key=value and {...spread}, and add them to PARENT." | ||
429 | + ;; Getting this function to not hang in the loop proved tricky. The | ||
430 | + ;; key is that `rjsx-parse-spread' and `rjsx-parse-single-attr' both | ||
431 | + ;; return `js2-error-node's if they fail to consume any tokens, | ||
432 | + ;; which signals to us that we just need to discard one token and | ||
433 | + ;; keep going. | ||
434 | + (let (attr | ||
435 | + (loop-terminators (list js2-DIV js2-GT js2-EOF js2-ERROR))) | ||
436 | + (while (not (memql (js2-peek-token) loop-terminators)) | ||
437 | + (rjsx-maybe-message "Starting loop. Next token type: %s\nToken pos: %s" (js2-peek-token) (js2-current-token-beg)) | ||
438 | + (setq attr | ||
439 | + (if (js2-match-token js2-LC) | ||
440 | + (or (rjsx-check-for-empty-curlies t) | ||
441 | + (prog1 (rjsx-parse-spread) | ||
442 | + (rjsx-maybe-message "Parsed spread"))) | ||
443 | + (rjsx-maybe-message "Parsing single attr") | ||
444 | + (rjsx-parse-single-attr))) | ||
445 | + (when (js2-error-node-p attr) (js2-get-token)) | ||
446 | + ; TODO: We should make this conditional on | ||
447 | + ; `js2-recover-from-parse-errors' | ||
448 | + (rjsx-node-push-prop parent attr)))) | ||
449 | + | ||
450 | + | ||
451 | +(cl-defun rjsx-check-for-empty-curlies (&optional dont-consume-rc &key check-for-comments warning) | ||
452 | + "If the following token is '}' set empty curly errors. | ||
453 | +If DONT-CONSUME-RC is non-nil, the matched right curly token | ||
454 | +won't be consumed. Returns a `js2-error-node' if the curlies are | ||
455 | +empty or nil otherwise. If CHECK-FOR-COMMENTS (a &KEY argument) | ||
456 | +is non-nil, this will check for comments inside the curlies and | ||
457 | +returns a `js2-empty-expr-node' if any are found. If WARNING (a | ||
458 | +&key argument) is non-nil, reports the empty curlies as a warning | ||
459 | +and not an error and also returns a `js2-empty-expr-node'. | ||
460 | +Assumes the current token is a '{'." | ||
461 | + (let ((beg (js2-current-token-beg)) end len) | ||
462 | + (when (js2-match-token js2-RC) | ||
463 | + (setq end (js2-current-token-end)) | ||
464 | + (setq len (- end beg)) | ||
465 | + (when dont-consume-rc | ||
466 | + (js2-unget-token)) | ||
467 | + (if check-for-comments (rjsx-maybe-message "Checking for comments between %d and %d" beg end)) | ||
468 | + (unless (and check-for-comments | ||
469 | + (dolist (comment js2-scanned-comments) | ||
470 | + (rjsx-maybe-message "Comment at %d, length=%d" | ||
471 | + (js2-node-pos comment) | ||
472 | + (js2-node-len comment)) | ||
473 | + ;; TODO: IF comments are in reverse document order, we should be able to | ||
474 | + ;; bail out early and know we didn't find one | ||
475 | + (when (and (>= (js2-node-pos comment) beg) | ||
476 | + (<= (+ (js2-node-pos comment) (js2-node-len comment)) end)) | ||
477 | + (cl-return-from rjsx-check-for-empty-curlies | ||
478 | + (make-js2-empty-expr-node :pos beg :len (- end beg)))))) | ||
479 | + (if warning | ||
480 | + (progn (js2-report-warning "msg.empty.expr" nil beg len) | ||
481 | + (make-js2-empty-expr-node :pos beg :len (- end beg))) | ||
482 | + (js2-report-error "msg.empty.expr" nil beg len) | ||
483 | + (make-js2-error-node :pos beg :len len)))))) | ||
484 | + | ||
485 | + | ||
486 | +(defun rjsx-parse-spread () | ||
487 | + "Parse an {...props} attribute." | ||
488 | + (let ((pn (make-rjsx-spread :pos (js2-current-token-beg))) | ||
489 | + (beg (js2-current-token-beg)) | ||
490 | + missing-dots expr) | ||
491 | + (setq missing-dots (not (js2-match-token js2-TRIPLEDOT))) | ||
492 | + ;; parse-assign-expr will go crazy if we're looking at `} /', so we | ||
493 | + ;; check for an empty spread first | ||
494 | + (if (js2-match-token js2-RC) | ||
495 | + (setq expr (make-js2-error-node :len 1)) | ||
496 | + (setq expr (js2-parse-assign-expr)) | ||
497 | + (when (js2-error-node-p expr) | ||
498 | + (pop js2-parsed-errors))) ; We'll add our own error | ||
499 | + (unless (or (js2-match-token js2-RC) (js2-error-node-p expr)) | ||
500 | + (js2-report-error "msg.no.rc.after.spread" nil | ||
501 | + beg (- (js2-current-token-end) beg))) | ||
502 | + (setf (rjsx-spread-expr pn) expr) | ||
503 | + (setf (js2-node-len pn) (- (js2-current-token-end) (js2-node-pos pn))) | ||
504 | + (js2-node-add-children pn expr) | ||
505 | + (if (js2-error-node-p expr) | ||
506 | + (js2-report-error "msg.syntax" nil beg (- (js2-current-token-end) beg)) | ||
507 | + (when missing-dots | ||
508 | + (js2-report-error "msg.no.dots.in.prop.spread" nil beg (js2-node-len pn)))) | ||
509 | + (if (= 0 (js2-node-len pn)) ; TODO: Is this ever possible? | ||
510 | + (make-js2-error-node :pos beg :len 0) | ||
511 | + pn))) | ||
512 | + | ||
513 | +(defun rjsx-parse-single-attr () | ||
514 | + "Parse an 'a=b' JSX attribute and return the corresponding XML node." | ||
515 | + (let ((pn (make-rjsx-attr)) name value beg) | ||
516 | + (setq name (rjsx-parse-identifier 'rjsx-attr)) ; Won't consume token on error | ||
517 | + (if (js2-error-node-p name) | ||
518 | + name | ||
519 | + (setf (rjsx-attr-name pn) name) | ||
520 | + (setq beg (js2-node-pos name)) | ||
521 | + (js2-node-add-children pn name) | ||
522 | + (rjsx-maybe-message "Got the name for the attr: `%s'" (rjsx-identifier-full-name name)) | ||
523 | + (if (js2-match-token js2-ASSIGN) ; Won't consume on error | ||
524 | + (progn | ||
525 | + (rjsx-maybe-message "Matched the equals sign") | ||
526 | + (if (js2-match-token js2-LC) | ||
527 | + (setq value (rjsx-parse-wrapped-expr nil t)) | ||
528 | + (if (js2-match-token js2-STRING) | ||
529 | + (setq value (rjsx-parse-string)) | ||
530 | + (js2-report-error "msg.no.value.after.jsx.prop" (rjsx-identifier-full-name name) | ||
531 | + beg (- (js2-current-token-end) beg)) | ||
532 | + (setq value (make-js2-error-node :pos beg :len (js2-current-token-len)))))) | ||
533 | + (setq value (make-js2-empty-expr-node :pos (js2-current-token-end) :len 0))) | ||
534 | + (rjsx-maybe-message "value type: `%s'" (js2-node-type value)) | ||
535 | + (setf (rjsx-attr-value pn) value) | ||
536 | + (setf (js2-node-len pn) (- (js2-node-end value) (js2-node-pos pn))) | ||
537 | + (js2-node-add-children pn value) | ||
538 | + (rjsx-maybe-message "Finished single attribute.") | ||
539 | + pn))) | ||
540 | + | ||
541 | +(defun rjsx-parse-wrapped-expr (allow-empty skip-to-rc) | ||
542 | + "Parse a curly-brace-wrapped JS expression. | ||
543 | +If ALLOW-EMPTY is non-nil, will warn for empty braces, otherwise | ||
544 | +will signal a syntax error. If it does not find a right curly | ||
545 | +and SKIP-TO-RC is non-nil, after the expression, consumes tokens | ||
546 | +until the end of the JSX node" | ||
547 | + (rjsx-maybe-message "parsing wrapped expression") | ||
548 | + (let (pn | ||
549 | + (beg (js2-current-token-beg)) | ||
550 | + (child (rjsx-check-for-empty-curlies nil | ||
551 | + :check-for-comments allow-empty | ||
552 | + :warning allow-empty))) | ||
553 | + (if child | ||
554 | + (if allow-empty | ||
555 | + (make-rjsx-wrapped-expr :pos beg :len (js2-node-len child) :child child) | ||
556 | + child) ;; Will be an error node in this case | ||
557 | + (setq child (js2-parse-assign-expr)) | ||
558 | + (rjsx-maybe-message "parsed expression, type: `%s'" (js2-node-type child)) | ||
559 | + (setq pn (make-rjsx-wrapped-expr :pos beg :child child)) | ||
560 | + (js2-node-add-children pn child) | ||
561 | + (when (js2-error-node-p child) | ||
562 | + (pop js2-parsed-errors)) ; We'll record our own message after checking for RC | ||
563 | + (if (js2-match-token js2-RC) | ||
564 | + (rjsx-maybe-message "matched } after expression") | ||
565 | + (rjsx-maybe-message "did not match } after expression") | ||
566 | + (when skip-to-rc | ||
567 | + (while (not (memql (js2-get-token) (list js2-RC js2-EOF js2-DIV js2-GT))) | ||
568 | + (rjsx-maybe-message "Skipped over `%s'" (js2-current-token-string))) | ||
569 | + (when (memq (js2-current-token-type) (list js2-DIV js2-GT)) | ||
570 | + (js2-unget-token))) | ||
571 | + (unless (js2-error-node-p child) | ||
572 | + (js2-report-error "msg.no.rc.after.expr" nil beg | ||
573 | + (- (js2-current-token-beg) beg)))) | ||
574 | + (when (js2-error-node-p child) | ||
575 | + (js2-report-error "msg.syntax" nil beg (- (js2-current-token-end) beg))) | ||
576 | + (setf (js2-node-len pn) (- (js2-current-token-end) beg)) | ||
577 | + pn))) | ||
578 | + | ||
579 | +(defun rjsx-parse-string () | ||
580 | + "Verify that current token is a valid JSX string. | ||
581 | +Returns a `js2-error-node' if TOKEN-STRING is not a valid JSX | ||
582 | +string, otherwise returns a `js2-string-node'. (Strings are | ||
583 | +invalid if they contain the delimiting quote character inside)" | ||
584 | + (rjsx-maybe-message "Parsing string") | ||
585 | + (let* ((token (js2-current-token)) | ||
586 | + (beg (js2-token-beg token)) | ||
587 | + (len (- (js2-token-end token) beg)) | ||
588 | + (token-string (js2-token-string token)) ;; JS2 does not include the quote-chars | ||
589 | + (quote-char (char-before (js2-token-end token)))) | ||
590 | + (if (cl-position quote-char token-string) | ||
591 | + (progn | ||
592 | + (js2-report-error "msg.invalid.jsx.string" nil beg len) | ||
593 | + (make-js2-error-node :pos beg :len len)) | ||
594 | + (make-js2-string-node :pos beg :len len :value token-string)))) | ||
595 | + | ||
596 | +(cl-defun rjsx-parse-identifier (&optional face &key (allow-ns t)) | ||
597 | + "Parse a possibly namespaced identifier and fontify with FACE if given. | ||
598 | +Returns a `js2-error-node' if unable to parse. If the &key | ||
599 | +argument ALLOW-NS is nil, does not allow namespaced names." | ||
600 | + (if (js2-must-match-name "msg.bad.jsx.ident") | ||
601 | + (let ((pn (make-rjsx-identifier)) | ||
602 | + (beg (js2-current-token-beg)) | ||
603 | + (name-parts (list (js2-current-token-string))) | ||
604 | + (allow-colon allow-ns) | ||
605 | + (continue t) | ||
606 | + (prev-token-end (js2-current-token-end)) | ||
607 | + (name-start (js2-current-token-beg)) | ||
608 | + matched-colon) | ||
609 | + (while (and continue | ||
610 | + (or (and (memq (js2-peek-token) (list js2-SUB js2-ASSIGN_SUB)) | ||
611 | + (prog2 ; Ensure no whitespace between previous name and this dash | ||
612 | + (js2-get-token) | ||
613 | + (eq prev-token-end (js2-current-token-beg)) | ||
614 | + (js2-unget-token))) | ||
615 | + (and allow-colon (= (js2-peek-token) js2-COLON)))) | ||
616 | + (if (setq matched-colon (js2-match-token js2-COLON)) | ||
617 | + (setf (rjsx-identifier-namespace pn) (apply #'concat (nreverse name-parts)) | ||
618 | + allow-colon nil | ||
619 | + name-parts (list) | ||
620 | + name-start nil) | ||
621 | + (when (= (js2-get-token) js2-ASSIGN_SUB) ; Otherwise it's a js2-SUB | ||
622 | + (setf (js2-token-end (js2-current-token)) (1- (js2-current-token-end)) | ||
623 | + (js2-token-type (js2-current-token)) js2-SUB | ||
624 | + (js2-token-string (js2-current-token)) "-" | ||
625 | + js2-ts-cursor (1+ (js2-current-token-beg)) | ||
626 | + js2-ti-lookahead 0)) | ||
627 | + (push "-" name-parts)) | ||
628 | + (setq prev-token-end (js2-current-token-end)) | ||
629 | + (if (js2-match-token js2-NAME) | ||
630 | + (if (eq prev-token-end (js2-current-token-beg)) | ||
631 | + (progn (push (js2-current-token-string) name-parts) | ||
632 | + (setq prev-token-end (js2-current-token-end) | ||
633 | + name-start (or name-start (js2-current-token-beg)))) | ||
634 | + (js2-unget-token) | ||
635 | + (setq continue nil)) | ||
636 | + (when (= js2-COLON (js2-current-token-type)) | ||
637 | + (js2-report-error "msg.bad.jsx.ident" nil beg (- (js2-current-token-end) beg))) | ||
638 | + ;; We only keep going if this is an `ident-ending-with-dash-colon:' | ||
639 | + (setq continue (and (not matched-colon) (= (js2-peek-token) js2-COLON))))) | ||
640 | + (when face | ||
641 | + (js2-set-face beg (js2-current-token-end) face 'record)) | ||
642 | + (setf (js2-node-len pn) (- (js2-current-token-end) beg) | ||
643 | + (rjsx-identifier-name pn) (if name-start | ||
644 | + (make-js2-name-node :pos name-start | ||
645 | + :len (- (js2-current-token-end) name-start) | ||
646 | + :name (apply #'concat (nreverse name-parts))) | ||
647 | + (make-js2-name-node :pos (js2-current-token-end) :len 0 :name ""))) | ||
648 | + pn) | ||
649 | + (make-js2-error-node :len (js2-current-token-len)))) | ||
650 | + | ||
651 | +(defun rjsx-parse-member-or-ns (&optional face) | ||
652 | + "Parse a dotted expression or a namespaced identifier and fontify with FACE if given." | ||
653 | + (let ((ident (rjsx-parse-identifier face))) | ||
654 | + (cond | ||
655 | + ((js2-error-node-p ident) ident) | ||
656 | + ((rjsx-identifier-namespace ident) ident) | ||
657 | + (t (rjsx-parse-member ident face))))) | ||
658 | + | ||
659 | +(defun rjsx-parse-member (ident &optional face) | ||
660 | + "Parse a dotted member expression starting with IDENT and fontify with FACE. | ||
661 | +IDENT is the `rjsx-identifier' node for the first item in the | ||
662 | +member expression. Returns a `js2-error-node' if unable to | ||
663 | +parse." | ||
664 | + (let (idents dots-pos pn end) | ||
665 | + (setq pn (make-rjsx-member :pos (js2-node-pos ident))) | ||
666 | + (setq end (js2-current-token-end)) | ||
667 | + (push ident idents) | ||
668 | + (while (and (js2-match-token js2-DOT) (not (js2-error-node-p ident))) | ||
669 | + (push (js2-current-token-beg) dots-pos) | ||
670 | + (setq end (js2-current-token-end)) | ||
671 | + (setq ident (rjsx-parse-identifier nil :allow-ns nil)) | ||
672 | + (push ident idents) | ||
673 | + (unless (js2-error-node-p ident) | ||
674 | + (setq end (js2-current-token-end))) | ||
675 | + (js2-node-add-children pn ident)) | ||
676 | + (setf (rjsx-member-idents pn) (nreverse idents) | ||
677 | + (rjsx-member-dots-pos pn) (nreverse dots-pos) | ||
678 | + (js2-node-len pn) (- end (js2-node-pos pn))) | ||
679 | + (when face | ||
680 | + (js2-set-face (js2-node-pos pn) end face 'record)) | ||
681 | + pn)) | ||
682 | + | ||
683 | + | ||
684 | +(defun rjsx-parse-child () | ||
685 | + "Parse an XML child node. | ||
686 | +Child nodes include plain (unquoted) text, other XML elements, | ||
687 | +and {}-bracketed expressions. Return the parsed child." | ||
688 | + (let ((tt (rjsx-get-next-xml-token))) | ||
689 | + (rjsx-maybe-message "child type `%s'" tt) | ||
690 | + (cond | ||
691 | + ((= tt js2-LT) | ||
692 | + (rjsx-maybe-message "xml-or-close") | ||
693 | + (rjsx-parse-xml-or-closing-tag)) | ||
694 | + | ||
695 | + ((= tt js2-LC) | ||
696 | + (rjsx-maybe-message "parsing expression { %s" (js2-peek-token)) | ||
697 | + (rjsx-parse-wrapped-expr t nil)) | ||
698 | + | ||
699 | + ((= tt rjsx-JSX-TEXT) | ||
700 | + (rjsx-maybe-message "text node: '%s'" (js2-current-token-string)) | ||
701 | + (js2-set-face (js2-current-token-beg) (js2-current-token-end) 'rjsx-text 'record) | ||
702 | + (js2-record-text-property (js2-current-token-beg) (js2-current-token-end) | ||
703 | + 'syntax-table (standard-syntax-table)) | ||
704 | + (make-rjsx-text :value (js2-current-token-string))) | ||
705 | + | ||
706 | + ((= tt js2-ERROR) | ||
707 | + (make-js2-error-node :len (js2-current-token-len))) | ||
708 | + | ||
709 | + (t (error "Unexpected token type: %s" (js2-peek-token)))))) | ||
710 | + | ||
711 | +(defun rjsx-parse-xml-or-closing-tag () | ||
712 | + "Parse a JSX tag, which could be a child or a closing tag. | ||
713 | +Return the parsed child, which is a `rjsx-closing-tag' if a | ||
714 | +closing tag was parsed." | ||
715 | + (let ((beg (js2-current-token-beg)) pn) | ||
716 | + (if (setq pn (rjsx-parse-empty-tag)) | ||
717 | + pn | ||
718 | + (if (js2-match-token js2-DIV) | ||
719 | + (progn (setq pn (make-rjsx-closing-tag :pos beg :name (rjsx-parse-member-or-ns 'rjsx-tag))) | ||
720 | + (if (js2-must-match js2-GT "msg.no.gt.in.closer" beg (- (js2-current-token-end) beg)) | ||
721 | + (rjsx-maybe-message "parsed closing tag") | ||
722 | + (rjsx-maybe-message "missing closing `>'")) | ||
723 | + (setf (js2-node-len pn) (- (js2-current-token-end) beg)) | ||
724 | + pn) | ||
725 | + (rjsx-maybe-message "parsing a child XML item") | ||
726 | + (rjsx-parse-xml))))) | ||
727 | + | ||
728 | +(defun rjsx-get-next-xml-token () | ||
729 | + "Scan through the XML text and push one token onto the stack." | ||
730 | + (setq js2-ts-string-buffer nil) ; for recording the text | ||
731 | + (when (> js2-ti-lookahead 0) | ||
732 | + (setq js2-ts-cursor (js2-current-token-end)) | ||
733 | + (setq js2-ti-lookahead 0)) | ||
734 | + | ||
735 | + (let ((token (js2-new-token 0)) | ||
736 | + c) | ||
737 | + (rjsx-maybe-message "Running the xml scanner") | ||
738 | + (catch 'return | ||
739 | + (while t | ||
740 | + (setq c (js2-get-char)) | ||
741 | + (rjsx-maybe-message "'%s' (%s)" (if (= c js2-EOF_CHAR) "EOF" (char-to-string c)) c) | ||
742 | + (cond | ||
743 | + ((or (= c ?}) (= c ?>)) | ||
744 | + (js2-set-string-from-buffer token) | ||
745 | + (setf (js2-token-type token) js2-ERROR) | ||
746 | + (js2-report-scan-error "msg.syntax" t) | ||
747 | + (throw 'return js2-ERROR)) | ||
748 | + | ||
749 | + ((or (= c ?<) (= c ?{)) | ||
750 | + (js2-unget-char) | ||
751 | + (if js2-ts-string-buffer | ||
752 | + (progn | ||
753 | + (js2-set-string-from-buffer token) | ||
754 | + (setf (js2-token-type token) rjsx-JSX-TEXT) | ||
755 | + (rjsx-maybe-message "created rjsx-JSX-TEXT token: `%s'" (js2-token-string token)) | ||
756 | + (throw 'return rjsx-JSX-TEXT)) | ||
757 | + (js2-get-char) | ||
758 | + (js2-set-string-from-buffer token) | ||
759 | + (setf (js2-token-type token) (if (= c ?<) js2-LT js2-LC)) | ||
760 | + (setf (js2-token-string token) (string c)) | ||
761 | + (throw 'return (js2-token-type token)))) | ||
762 | + | ||
763 | + ((= c js2-EOF_CHAR) | ||
764 | + (js2-set-string-from-buffer token) | ||
765 | + (rjsx-maybe-message "Hit EOF. Current buffer: `%s'" (js2-token-string token)) | ||
766 | + (setf (js2-token-type token) js2-ERROR) | ||
767 | + (rjsx-maybe-message "Scanner hit EOF. Panic!") | ||
768 | + (throw 'rjsx-eof-while-parsing t)) | ||
769 | + (t (js2-add-to-string c))))))) | ||
770 | + | ||
771 | +(defun rjsx--tag-at-point () | ||
772 | + "Return the JSX tag at point, if any, or nil." | ||
773 | + (let ((node (js2-node-at-point (point) t))) | ||
774 | + (while (and node (not (rjsx-node-p node))) | ||
775 | + (setq node (js2-node-parent node))) | ||
776 | + node)) | ||
777 | + | ||
778 | + | ||
779 | +;;;; Interactive commands and keybindings | ||
780 | +(defun rjsx-electric-lt (n) | ||
781 | + "Insert a context-sensitive less-than sign. | ||
782 | +Optional prefix argument N indicates how many signs to insert. | ||
783 | +If N is greater than one, no special handling takes place. | ||
784 | +Otherwise, if the less-than sign would start a JSX block, it | ||
785 | +inserts `< />' and places the cursor inside the new tag." | ||
786 | + (interactive "p") | ||
787 | + (if (/= n 1) | ||
788 | + (insert (make-string n "<")) | ||
789 | + (let ((inhibit-changing-match-data t)) | ||
790 | + (if (looking-back (rx (or "=" "(" "?" ":" ">" "}" "&" "|" "{" "," | ||
791 | + "return") | ||
792 | + (zero-or-more (or "\n" space))) | ||
793 | + (point-at-bol -2)) | ||
794 | + (progn (insert "</>") | ||
795 | + (backward-char 2)) | ||
796 | + (insert "<"))))) | ||
797 | + | ||
798 | +(define-key rjsx-mode-map "<" 'rjsx-electric-lt) | ||
799 | + | ||
800 | +(defun rjsx-delete-creates-full-tag (n &optional killflag) | ||
801 | + "N and KILLFLAG are as in `delete-char'. | ||
802 | +If N is 1 and KILLFLAG nil, checks to see if we're in a | ||
803 | +self-closing tag about to delete the slash. If so, deletes the | ||
804 | +slash and inserts a matching end-tag." | ||
805 | + (interactive "p") | ||
806 | + (if (or killflag (/= 1 n) (not (eq (get-char-property (point) 'rjsx-class) 'self-closing-slash))) | ||
807 | + (if (called-interactively-p 'any) | ||
808 | + (call-interactively 'delete-forward-char) | ||
809 | + (delete-char n killflag)) | ||
810 | + (let ((node (rjsx--tag-at-point))) | ||
811 | + (if node | ||
812 | + (progn | ||
813 | + (delete-char 1) | ||
814 | + (search-forward ">" ) | ||
815 | + (save-excursion | ||
816 | + (insert "</" (rjsx-node-opening-tag-name node) ">"))) | ||
817 | + (delete-char 1))))) | ||
818 | + | ||
819 | +(define-key rjsx-mode-map (kbd "C-d") 'rjsx-delete-creates-full-tag) | ||
820 | + | ||
821 | +(defun rjsx-rename-tag-at-point (new-name) | ||
822 | + "Prompt for a new name and modify the tag at point. | ||
823 | +NEW-NAME is the name to give the tag." | ||
824 | + (interactive "sNew tag name: ") | ||
825 | + (let ((tag (rjsx--tag-at-point)) closer) | ||
826 | + (if tag | ||
827 | + (let* ((head (rjsx-node-name tag)) | ||
828 | + (tail (when (setq closer (rjsx-node-closing-tag tag)) (rjsx-closing-tag-name closer))) | ||
829 | + beg end) | ||
830 | + (dolist (part (if tail (list tail head) (list head))) | ||
831 | + (setq beg (js2-node-abs-pos part) | ||
832 | + end (+ beg (js2-node-len part))) | ||
833 | + (delete-region beg end) | ||
834 | + (save-excursion (goto-char beg) (insert new-name))) | ||
835 | + (js2-reparse)) | ||
836 | + (message "No JSX tag found at point")))) | ||
837 | + | ||
838 | +(define-key rjsx-mode-map (kbd "C-c C-r") 'rjsx-rename-tag-at-point) | ||
839 | + | ||
840 | + | ||
841 | +(provide 'rjsx-mode) | ||
842 | +;;; rjsx-mode.el ends here | ||
843 | + | ||
844 | +;; Local Variables: | ||
845 | +;; outline-regexp: ";;;\\(;* [^ | ||
846 | +;; ]\\|###autoload\\)\\|(....." | ||
847 | +;; End: |
elpa/rjsx-mode-20170808.634/rjsx-mode.elc
0 → 100644
No preview for this file type
-
Please register or login to post a comment