1 ;;; js2-old-indent.el --- Indentation code kept for compatibility
3 ;; Copyright (C) 2015 Free Software Foundation, Inc.
5 ;; This file is part of GNU Emacs.
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.
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.
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/>.
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.
26 ;; This code is kept for Emacs 24.5 and ealier.
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.
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.
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.
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
59 (defvar js2-language-version)
61 (declare-function js2-mark-safe-local "js2-mode")
62 (declare-function js2-backward-sws "js2-mode")
63 (declare-function js2-forward-sws "js2-mode")
64 (declare-function js2-same-line "js2-mode")
66 (defcustom js2-basic-offset (if (and (boundp 'c-basic-offset)
67 (numberp c-basic-offset))
70 "Number of spaces to indent nested statements.
71 Similar to `c-basic-offset'."
76 (defcustom js2-pretty-multiline-declarations t
77 "Non-nil to line up multiline declarations vertically:
83 If the value is t, and the first assigned value in the
84 declaration is a function/array/object literal spanning several
85 lines, it won't be indented additionally:
87 var o = { var bar = 2,
92 If the value is `all', it will always be indented additionally:
103 If the value is `dynamic', it will be indented additionally only
104 if the declaration contains more than one variable:
118 (defcustom js2-indent-switch-body nil
119 "When nil, case labels are indented on the same level as the
120 containing switch statement. Otherwise, all lines inside
121 switch statement body are indented one additional level."
126 (defconst js2-possibly-braceless-keywords-re
127 (concat "else[ \t]+if\\|for[ \t]+each\\|"
128 (regexp-opt '("catch" "do" "else" "finally" "for" "if"
129 "try" "while" "with" "let")))
130 "Regular expression matching keywords that are optionally
131 followed by an opening brace.")
133 (defconst js2-indent-operator-re
134 (concat "[-+*/%<>&^|?:.]\\([^-+*/]\\|$\\)\\|!?=\\|"
135 (regexp-opt '("in" "instanceof") 'words))
136 "Regular expression matching operators that affect indentation
137 of continued expressions.")
139 (defconst js2-declaration-keyword-re
140 (regexp-opt '("var" "let" "const") 'words)
141 "Regular expression matching variable declaration keywords.")
143 (defun js2-re-search-forward-inner (regexp &optional bound count)
144 "Auxiliary function for `js2-re-search-forward'."
145 (let (parse saved-point)
147 (re-search-forward regexp bound)
148 (setq parse (if saved-point
149 (parse-partial-sexp saved-point (point))
150 (syntax-ppss (point))))
153 (concat "\\(\\=\\|[^\\]\\|^\\)" (string (nth 3 parse)))
154 (save-excursion (end-of-line) (point)) t))
158 (and (eq (char-before) ?\/) (eq (char-after) ?\*)))
159 (re-search-forward "\\*/"))
161 (setq count (1- count))))
162 (setq saved-point (point))))
165 (defun js2-re-search-forward (regexp &optional bound noerror count)
166 "Search forward but ignore strings and comments.
167 Invokes `re-search-forward' but treats the buffer as if strings
168 and comments have been removed."
169 (let ((saved-point (point)))
172 (js2-re-search-forward-inner regexp bound 1))
174 (js2-re-search-backward-inner regexp bound (- count)))
176 (js2-re-search-forward-inner regexp bound count)))
178 (goto-char saved-point)
180 (error (error-message-string err)))))))
182 (defun js2-re-search-backward-inner (regexp &optional bound count)
183 "Auxiliary function for `js2-re-search-backward'."
186 (re-search-backward regexp bound)
187 (setq parse (syntax-ppss (point)))
190 (concat "\\([^\\]\\|^\\)" (string (nth 3 parse)))
191 (line-beginning-position) t))
193 (goto-char (nth 8 parse)))
195 (and (eq (char-before) ?/) (eq (char-after) ?*)))
196 (re-search-backward "/\\*"))
198 (setq count (1- count))))))
201 (defun js2-re-search-backward (regexp &optional bound noerror count)
202 "Search backward but ignore strings and comments.
203 Invokes `re-search-backward' but treats the buffer as if strings
204 and comments have been removed."
205 (let ((saved-point (point)))
208 (js2-re-search-backward-inner regexp bound 1))
210 (js2-re-search-forward-inner regexp bound (- count)))
212 (js2-re-search-backward-inner regexp bound count)))
214 (goto-char saved-point)
216 (error (error-message-string err)))))))
218 (defun js2-looking-at-operator-p ()
219 "Return non-nil if text after point is a non-comma operator."
220 (defvar js2-mode-identifier-re)
221 (and (looking-at js2-indent-operator-re)
222 (or (not (eq (char-after) ?:))
224 (and (js2-re-search-backward "[?:{]\\|\\_<case\\_>" nil t)
225 (eq (char-after) ??))))
228 ;; Generator method (possibly using computed property).
229 (looking-at (concat "\\* *\\(?:\\[\\|"
230 js2-mode-identifier-re
234 ;; We might misindent some expressions that would
235 ;; return NaN anyway. Shouldn't be a problem.
236 (memq (char-before) '(?, ?} ?{)))))))
238 (defun js2-continued-expression-p ()
239 "Return non-nil if the current line continues an expression."
241 (back-to-indentation)
242 (or (js2-looking-at-operator-p)
244 (while (and (re-search-backward "\n" nil t)
245 (let ((state (syntax-ppss)))
247 (goto-char (nth 8 state))) ;; skip comments
248 (skip-chars-backward " \t")
251 (throw 'found t))))))
253 (when (js2-looking-at-operator-p)
255 (not (looking-at "\\*\\|\\+\\+\\|--\\|/[/*]")))))))
257 (defun js2-end-of-do-while-loop-p ()
258 "Return non-nil if word after point is `while' of a do-while
259 statement, else returns nil. A braceless do-while statement
260 spanning several lines requires that the start of the loop is
261 indented to the same column as the current line."
264 (when (looking-at "\\s-*\\_<while\\_>")
266 (skip-chars-backward "[ \t\n]*}")
267 (looking-at "[ \t\n]*}"))
269 (backward-list) (backward-word 1) (looking-at "\\_<do\\_>"))
270 (js2-re-search-backward "\\_<do\\_>" (point-at-bol) t)
271 (or (looking-at "\\_<do\\_>")
272 (let ((saved-indent (current-indentation)))
273 (while (and (js2-re-search-backward "^[ \t]*\\_<" nil t)
274 (/= (current-indentation) saved-indent)))
275 (and (looking-at "[ \t]*\\_<do\\_>")
276 (not (js2-re-search-forward
277 "\\_<while\\_>" (point-at-eol) t))
278 (= (current-indentation) saved-indent))))))))
280 (defun js2-multiline-decl-indentation ()
281 "Return the declaration indentation column if the current line belongs
282 to a multiline declaration statement. See `js2-pretty-multiline-declarations'."
283 (let (forward-sexp-function ; use Lisp version
286 (back-to-indentation)
287 (when (not (looking-at js2-declaration-keyword-re))
288 (when (looking-at js2-indent-operator-re)
289 (goto-char (match-end 0))) ; continued expressions are ok
290 (while (and (not at-opening-bracket)
295 (or (eq (char-before) ?,)
296 (and (not (eq (char-before) ?\;))
297 (prog2 (skip-syntax-backward ".")
298 (looking-at js2-indent-operator-re)
300 (not (eq (char-before) ?\;)))
301 (js2-same-line pos)))))
304 (scan-error (setq at-opening-bracket t))))
305 (when (looking-at js2-declaration-keyword-re)
306 (goto-char (match-end 0))
307 (1+ (current-column)))))))
309 (defun js2-ctrl-statement-indentation ()
310 "Return the proper indentation of current line if it is a control statement.
311 Returns an indentation if this line starts the body of a control
312 statement without braces, else returns nil."
313 (let (forward-sexp-function)
315 (back-to-indentation)
316 (when (and (not (js2-same-line (point-min)))
317 (not (looking-at "{"))
318 (js2-re-search-backward "[[:graph:]]" nil t)
319 (not (looking-at "[{([]"))
322 (when (= (char-before) ?\))
323 ;; scan-sexps sometimes throws an error
324 (ignore-errors (backward-sexp))
325 (skip-chars-backward " \t" (point-at-bol)))
327 (back-to-indentation)
328 (when (looking-at "}[ \t]*")
329 (goto-char (match-end 0)))
330 (and (looking-at js2-possibly-braceless-keywords-re)
332 (not (js2-end-of-do-while-loop-p))))))
333 (+ (current-indentation) js2-basic-offset)))))
335 (defun js2-indent-in-array-comp (parse-status)
336 "Return non-nil if we think we're in an array comprehension.
337 In particular, return the buffer position of the first `for' kwd."
338 (let ((bracket (nth 1 parse-status))
343 (when (looking-at "\\[")
346 (if (looking-at "[[{]")
347 (let (forward-sexp-function) ; use Lisp version
348 (forward-sexp) ; skip destructuring form
350 (if (and (/= (char-after) ?,) ; regular array
352 (match-beginning 0)))
353 ;; to skip arbitrary expressions we need the parser,
354 ;; so we'll just guess at it.
355 (if (and (> end (point)) ; not empty literal
356 (re-search-forward "[^,]]* \\(for\\) " end t)
357 ;; not inside comment or string literal
358 (let ((state (parse-partial-sexp bracket (point))))
359 (not (or (nth 3 state) (nth 4 state)))))
360 (match-beginning 1))))))))
362 (defun js2-array-comp-indentation (parse-status for-kwd)
363 (if (js2-same-line for-kwd)
364 ;; first continuation line
366 (goto-char (nth 1 parse-status))
368 (skip-chars-forward " \t")
374 (defun js2-maybe-goto-declaration-keyword-end (bracket)
375 "Helper function for `js2-proper-indentation'.
376 Depending on the value of `js2-pretty-multiline-declarations',
377 move point to the end of a variable declaration keyword so that
378 indentation is aligned to that column."
380 ((eq js2-pretty-multiline-declarations 'all)
381 (when (looking-at js2-declaration-keyword-re)
382 (goto-char (1+ (match-end 0)))))
383 ((eq js2-pretty-multiline-declarations 'dynamic)
384 (let (declaration-keyword-end
387 (when (looking-at js2-declaration-keyword-re)
388 ;; Preserve the match data lest it somehow be overridden.
389 (setq declaration-keyword-end (match-end 0))
392 (setq at-closing-bracket-p
393 ;; Handle scan errors gracefully.
396 ;; Use the regular `forward-sexp-function' because the
397 ;; normal one for this mode uses the AST.
398 (let (forward-sexp-function)
402 (when at-closing-bracket-p
404 (setq comma-p (looking-at-p ","))))
406 (goto-char (1+ declaration-keyword-end))))))))
408 (cl-defun js2-proper-indentation (parse-status)
409 "Return the proper indentation for the current line."
411 (back-to-indentation)
412 (when (nth 4 parse-status)
413 (cl-return-from js2-proper-indentation (js2--comment-indent parse-status)))
414 (let* ((at-closing-bracket (looking-at "[]})]"))
415 (same-indent-p (or at-closing-bracket
416 (looking-at "\\_<case\\_>[^:]")
417 (and (looking-at "\\_<default:")
420 (not (memq (char-before) '(?, ?{)))))))
421 (continued-expr-p (js2-continued-expression-p))
422 (declaration-indent (and js2-pretty-multiline-declarations
423 (js2-multiline-decl-indentation)))
424 (bracket (nth 1 parse-status))
427 ;; indent array comprehension continuation lines specially
429 (>= js2-language-version 170)
430 (not (js2-same-line bracket))
431 (setq beg (js2-indent-in-array-comp parse-status))
432 (>= (point) (save-excursion
434 (point-at-bol)))) ; at or after first loop?
435 (js2-array-comp-indentation parse-status beg))
437 ((js2-ctrl-statement-indentation))
439 ((and declaration-indent continued-expr-p)
440 (+ declaration-indent js2-basic-offset))
447 ((looking-at "[({[][ \t]*\\(/[/*]\\|$\\)")
448 (when (save-excursion (skip-chars-backward " \t\n)")
451 (back-to-indentation)
452 (js2-maybe-goto-declaration-keyword-end bracket)
457 (+ (current-column) (* 2 js2-basic-offset)))
459 (+ (current-column) js2-basic-offset))))
460 (if (and js2-indent-switch-body
461 (not at-closing-bracket)
462 (looking-at "\\_<switch\\_>"))
463 (+ indent js2-basic-offset)
466 (unless same-indent-p
468 (skip-chars-forward " \t"))
471 (continued-expr-p js2-basic-offset)
475 (defun js2--comment-indent (parse-status)
476 "Indentation inside a multi-line block comment continuation line."
478 (goto-char (nth 8 parse-status))
479 (if (looking-at "/\\*")
480 (+ 1 (current-column))
483 (defun js2-indent-line (&optional bounce-backwards)
484 "Indent the current line as JavaScript source text."
486 (let (parse-status offset
487 ;; Don't whine about errors/warnings when we're indenting.
488 ;; This has to be set before calling parse-partial-sexp below.
489 (inhibit-point-motion-hooks t))
490 (setq parse-status (save-excursion
491 (syntax-ppss (point-at-bol)))
492 offset (- (point) (save-excursion
493 (back-to-indentation)
495 ;; Don't touch multiline strings.
496 (unless (nth 3 parse-status)
497 (indent-line-to (js2-proper-indentation parse-status))
498 (when (cl-plusp offset)
499 (forward-char offset)))))
503 ;; The following JSX indentation code is copied basically verbatim from js.el at
504 ;; 958da7f, except that the prefixes on the functions/variables are changed.
506 (defsubst js2--jsx-find-before-tag ()
507 "Find where JSX starts.
509 Assume JSX appears in the following instances:
510 - Inside parentheses, when returned or as the first argument
511 to a function, and after a newline
512 - When assigned to variables or object properties, but only
514 - As the N+1th argument to a function
516 This is an optimized version of (re-search-backward \"[(,]\n\"
517 nil t), except set point to the end of the match. This logic
518 executes up to the number of lines in the file, so it should be
519 really fast to reduce that impact."
521 (while (and (> (point) (point-min))
524 (when (or (eq (char-before) 40) ; (
525 (eq (char-before) 44)) ; ,
526 (setq pos (1- (point))))))))
529 (defconst js2--jsx-end-tag-re
530 (concat "</" sgml-name-re ">\\|/>")
531 "Find the end of a JSX element.")
533 (defconst js2--jsx-after-tag-re "[),]"
534 "Find where JSX ends.
535 This complements the assumption of where JSX appears from
536 `js--jsx-before-tag-re', which see.")
538 (defun js2--jsx-indented-element-p ()
539 "Determine if/how the current line should be indented as JSX.
541 Return `first' for the first JSXElement on its own line.
542 Return `nth' for subsequent lines of the first JSXElement.
543 Return `expression' for an embedded JS expression.
544 Return `after' for anything after the last JSXElement.
545 Return nil for non-JSX lines.
547 Currently, JSX indentation supports the following styles:
549 - Single-line elements (indented like normal JS):
551 var element = <div></div>;
553 - Multi-line elements (enclosed in parentheses):
563 - Function arguments:
567 document.querySelector('.root')
569 (let ((current-pos (point))
570 (current-line (line-number-at-pos))
572 before-tag-pos before-tag-line
573 tag-start-pos tag-start-line
574 tag-end-pos tag-end-line
579 ;; Determine if we're inside a jsx element
582 (while (and (not tag-start-pos)
583 (setq last-pos (js2--jsx-find-before-tag)))
584 (while (forward-comment 1))
585 (when (= (char-after) 60) ; <
586 (setq before-tag-pos last-pos
587 tag-start-pos (point)))
588 (goto-char last-pos))
591 (setq before-tag-line (line-number-at-pos before-tag-pos)
592 tag-start-line (line-number-at-pos tag-start-pos))
594 ;; A "before" line which also starts an element begins with js, so
596 (> current-line before-tag-line)
597 ;; Only indent the jsx lines like jsx
598 (>= current-line tag-start-line)))
600 ;; Analyze bounds if there are any
602 (while (and (not tag-end-pos)
603 (setq last-pos (re-search-forward js2--jsx-end-tag-re nil t)))
604 (while (forward-comment 1))
605 (when (looking-at js2--jsx-after-tag-re)
606 (setq tag-end-pos last-pos)))
608 (setq tag-end-line (line-number-at-pos tag-end-pos)
609 after-tag-line (line-number-at-pos after-tag-line))
611 ;; Ensure we're actually within the bounds of the jsx
612 (<= current-line tag-end-line)
613 ;; An "after" line which does not end an element begins with
614 ;; js, so indent it like js
615 (<= current-line after-tag-line))
617 ;; Handle another case where there could be e.g. comments after
619 (> current-line tag-end-line)
620 (< current-line after-tag-line)
621 (setq type 'after))))
622 ;; They may not be any bounds (yet)
624 ;; Check if we're inside an embedded multi-line js expression
627 (goto-char current-pos)
629 (setq parens (nth 9 (syntax-ppss)))
630 (while (and parens (not type))
631 (setq paren (car parens))
633 ((and (>= paren tag-start-pos)
634 ;; Curly bracket indicates the start of an embedded expression
635 (= (char-after paren) 123) ; {
636 ;; The first line of the expression is indented like sgml
637 (> current-line (line-number-at-pos paren))
638 ;; Check if within a closing curly bracket (if any)
639 ;; (exclusive, as the closing bracket is indented like sgml)
643 (ignore-errors (let (forward-sexp-function)
645 (< current-line (line-number-at-pos)))
647 ;; Indicate this guy will be indented specially
648 (setq type 'expression))
649 (t (setq parens (cdr parens)))))
654 ;; Indent the first jsx thing like js so we can indent future jsx things
655 ;; like sgml relative to the first thing
656 ((= current-line tag-start-line) 'first)
659 (defmacro js2--as-sgml (&rest body)
660 "Execute BODY as if in sgml-mode."
661 `(with-syntax-table sgml-mode-syntax-table
662 (let (forward-sexp-function
663 parse-sexp-lookup-properties)
666 (defun js2--expression-in-sgml-indent-line ()
667 "Indent the current line as JavaScript or SGML (whichever is farther)."
670 ;; Don't whine about errors/warnings when we're indenting.
671 ;; This has to be set before calling parse-partial-sexp below.
672 (inhibit-point-motion-hooks t)
673 (parse-status (save-excursion
674 (syntax-ppss (point-at-bol)))))
675 ;; Don't touch multiline strings.
676 (unless (nth 3 parse-status)
677 (setq indent-col (save-excursion
678 (back-to-indentation)
679 (if (>= (point) savep) (setq savep nil))
680 (js2--as-sgml (sgml-calculate-indent))))
681 (if (null indent-col)
683 ;; Use whichever indentation column is greater, such that the sgml
684 ;; column is effectively a minimum
685 (setq indent-col (max (js2-proper-indentation parse-status)
686 (+ indent-col js2-basic-offset)))
688 (save-excursion (indent-line-to indent-col))
689 (indent-line-to indent-col))))))
691 (defun js2-jsx-indent-line ()
692 "Indent the current line as JSX (with SGML offsets).
693 i.e., customize JSX element indentation with `sgml-basic-offset'
696 (let ((indentation-type (js2--jsx-indented-element-p)))
698 ((eq indentation-type 'expression)
699 (js2--expression-in-sgml-indent-line))
700 ((or (eq indentation-type 'first)
701 (eq indentation-type 'after))
702 ;; Don't treat this first thing as a continued expression (often a "<" or
703 ;; ">" causes this misinterpretation)
704 (cl-letf (((symbol-function #'js2-continued-expression-p) 'ignore))
706 ((eq indentation-type 'nth)
707 (js2--as-sgml (sgml-indent-line)))
708 (t (js2-indent-line)))))
710 (provide 'js2-old-indent)
712 ;;; js2-old-indent.el ends here