;;; js2-old-indent.el --- Indentation code kept for compatibility
;; Copyright (C) 2015 Free Software Foundation, Inc.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see .
;;; Commentary:
;; All features of this indentation code have been ported to Emacs's
;; built-in `js-mode' by now, so we derive from it. An older
;; commentary follows.
;; This code is kept for Emacs 24.5 and ealier.
;; This indenter is based on Karl Landström's "javascript.el" indenter.
;; Karl cleverly deduces that the desired indentation level is often a
;; function of paren/bracket/brace nesting depth, which can be determined
;; quickly via the built-in `parse-partial-sexp' function. His indenter
;; then does some equally clever checks to see if we're in the context of a
;; substatement of a possibly braceless statement keyword such as if, while,
;; or finally. This approach yields pretty good results.
;; The indenter is often "wrong", however, and needs to be overridden.
;; The right long-term solution is probably to emulate (or integrate
;; with) cc-engine, but it's a nontrivial amount of coding. Even when a
;; parse tree from `js2-parse' is present, which is not true at the
;; moment the user is typing, computing indentation is still thousands
;; of lines of code to handle every possible syntactic edge case.
;; In the meantime, the compromise solution is that we offer a "bounce
;; indenter", configured with `js2-bounce-indent-p', which cycles the
;; current line indent among various likely guess points. This approach
;; is far from perfect, but should at least make it slightly easier to
;; move the line towards its desired indentation when manually
;; overriding Karl's heuristic nesting guesser.
;; I've made miscellaneous tweaks to Karl's code to handle some Ecma
;; extensions such as `let' and Array comprehensions. Major kudos to
;; Karl for coming up with the initial approach, which packs a lot of
;; punch for so little code. -- Steve
;;; Code:
(defvar js2-language-version)
(declare-function js2-mark-safe-local "js2-mode")
(declare-function js2-backward-sws "js2-mode")
(declare-function js2-forward-sws "js2-mode")
(declare-function js2-same-line "js2-mode")
(defcustom js2-basic-offset (if (and (boundp 'c-basic-offset)
(numberp c-basic-offset))
c-basic-offset
4)
"Number of spaces to indent nested statements.
Similar to `c-basic-offset'."
:group 'js2-mode
:safe 'integerp
:type 'integer)
(defcustom js2-pretty-multiline-declarations t
"Non-nil to line up multiline declarations vertically:
var a = 10,
b = 20,
c = 30;
If the value is t, and the first assigned value in the
declaration is a function/array/object literal spanning several
lines, it won't be indented additionally:
var o = { var bar = 2,
foo: 3 vs. o = {
}, foo: 3
bar = 2; };
If the value is `all', it will always be indented additionally:
var o = {
foo: 3
};
var o = {
foo: 3
},
bar = 2;
If the value is `dynamic', it will be indented additionally only
if the declaration contains more than one variable:
var o = {
foo: 3
};
var o = {
foo: 3
},
bar = 2;"
:group 'js2-mode
:safe 'symbolp
:type 'symbol)
(defcustom js2-indent-switch-body nil
"When nil, case labels are indented on the same level as the
containing switch statement. Otherwise, all lines inside
switch statement body are indented one additional level."
:type 'boolean
:safe 'booleanp
:group 'js2-mode)
(defconst js2-possibly-braceless-keywords-re
(concat "else[ \t]+if\\|for[ \t]+each\\|"
(regexp-opt '("catch" "do" "else" "finally" "for" "if"
"try" "while" "with" "let")))
"Regular expression matching keywords that are optionally
followed by an opening brace.")
(defconst js2-indent-operator-re
(concat "[-+*/%<>&^|?:.]\\([^-+*/]\\|$\\)\\|!?=\\|"
(regexp-opt '("in" "instanceof") 'words))
"Regular expression matching operators that affect indentation
of continued expressions.")
(defconst js2-declaration-keyword-re
(regexp-opt '("var" "let" "const") 'words)
"Regular expression matching variable declaration keywords.")
(defun js2-re-search-forward-inner (regexp &optional bound count)
"Auxiliary function for `js2-re-search-forward'."
(let (parse saved-point)
(while (> count 0)
(re-search-forward regexp bound)
(setq parse (if saved-point
(parse-partial-sexp saved-point (point))
(syntax-ppss (point))))
(cond ((nth 3 parse)
(re-search-forward
(concat "\\(\\=\\|[^\\]\\|^\\)" (string (nth 3 parse)))
(save-excursion (end-of-line) (point)) t))
((nth 7 parse)
(forward-line))
((or (nth 4 parse)
(and (eq (char-before) ?\/) (eq (char-after) ?\*)))
(re-search-forward "\\*/"))
(t
(setq count (1- count))))
(setq saved-point (point))))
(point))
(defun js2-re-search-forward (regexp &optional bound noerror count)
"Search forward but ignore strings and comments.
Invokes `re-search-forward' but treats the buffer as if strings
and comments have been removed."
(let ((saved-point (point)))
(condition-case err
(cond ((null count)
(js2-re-search-forward-inner regexp bound 1))
((< count 0)
(js2-re-search-backward-inner regexp bound (- count)))
((> count 0)
(js2-re-search-forward-inner regexp bound count)))
(search-failed
(goto-char saved-point)
(unless noerror
(error (error-message-string err)))))))
(defun js2-re-search-backward-inner (regexp &optional bound count)
"Auxiliary function for `js2-re-search-backward'."
(let (parse)
(while (> count 0)
(re-search-backward regexp bound)
(setq parse (syntax-ppss (point)))
(cond ((nth 3 parse)
(re-search-backward
(concat "\\([^\\]\\|^\\)" (string (nth 3 parse)))
(line-beginning-position) t))
((nth 7 parse)
(goto-char (nth 8 parse)))
((or (nth 4 parse)
(and (eq (char-before) ?/) (eq (char-after) ?*)))
(re-search-backward "/\\*"))
(t
(setq count (1- count))))))
(point))
(defun js2-re-search-backward (regexp &optional bound noerror count)
"Search backward but ignore strings and comments.
Invokes `re-search-backward' but treats the buffer as if strings
and comments have been removed."
(let ((saved-point (point)))
(condition-case err
(cond ((null count)
(js2-re-search-backward-inner regexp bound 1))
((< count 0)
(js2-re-search-forward-inner regexp bound (- count)))
((> count 0)
(js2-re-search-backward-inner regexp bound count)))
(search-failed
(goto-char saved-point)
(unless noerror
(error (error-message-string err)))))))
(defun js2-looking-at-operator-p ()
"Return non-nil if text after point is a non-comma operator."
(defvar js2-mode-identifier-re)
(and (looking-at js2-indent-operator-re)
(or (not (eq (char-after) ?:))
(save-excursion
(and (js2-re-search-backward "[?:{]\\|\\_" nil t)
(eq (char-after) ??))))
(not (and
(eq (char-after) ?*)
(looking-at (concat "\\* *" js2-mode-identifier-re " *("))
(save-excursion
(goto-char (1- (match-end 0)))
(let (forward-sexp-function) (forward-sexp))
(js2-forward-sws)
(eq (char-after) ?{))))))
(defun js2-continued-expression-p ()
"Return non-nil if the current line continues an expression."
(save-excursion
(back-to-indentation)
(or (js2-looking-at-operator-p)
(when (catch 'found
(while (and (re-search-backward "\n" nil t)
(let ((state (syntax-ppss)))
(when (nth 4 state)
(goto-char (nth 8 state))) ;; skip comments
(skip-chars-backward " \t")
(if (bolp)
t
(throw 'found t))))))
(backward-char)
(when (js2-looking-at-operator-p)
(backward-char)
(not (looking-at "\\*\\|\\+\\+\\|--\\|/[/*]")))))))
(defun js2-end-of-do-while-loop-p ()
"Return non-nil if word after point is `while' of a do-while
statement, else returns nil. A braceless do-while statement
spanning several lines requires that the start of the loop is
indented to the same column as the current line."
(interactive)
(save-excursion
(when (looking-at "\\s-*\\_")
(if (save-excursion
(skip-chars-backward "[ \t\n]*}")
(looking-at "[ \t\n]*}"))
(save-excursion
(backward-list) (backward-word 1) (looking-at "\\_"))
(js2-re-search-backward "\\_" (point-at-bol) t)
(or (looking-at "\\_")
(let ((saved-indent (current-indentation)))
(while (and (js2-re-search-backward "^[ \t]*\\_<" nil t)
(/= (current-indentation) saved-indent)))
(and (looking-at "[ \t]*\\_")
(not (js2-re-search-forward
"\\_" (point-at-eol) t))
(= (current-indentation) saved-indent))))))))
(defun js2-multiline-decl-indentation ()
"Return the declaration indentation column if the current line belongs
to a multiline declaration statement. See `js2-pretty-multiline-declarations'."
(let (forward-sexp-function ; use Lisp version
at-opening-bracket)
(save-excursion
(back-to-indentation)
(when (not (looking-at js2-declaration-keyword-re))
(when (looking-at js2-indent-operator-re)
(goto-char (match-end 0))) ; continued expressions are ok
(while (and (not at-opening-bracket)
(not (bobp))
(let ((pos (point)))
(save-excursion
(js2-backward-sws)
(or (eq (char-before) ?,)
(and (not (eq (char-before) ?\;))
(prog2 (skip-syntax-backward ".")
(looking-at js2-indent-operator-re)
(js2-backward-sws))
(not (eq (char-before) ?\;)))
(js2-same-line pos)))))
(condition-case _
(backward-sexp)
(scan-error (setq at-opening-bracket t))))
(when (looking-at js2-declaration-keyword-re)
(goto-char (match-end 0))
(1+ (current-column)))))))
(defun js2-ctrl-statement-indentation ()
"Return the proper indentation of current line if it is a control statement.
Returns an indentation if this line starts the body of a control
statement without braces, else returns nil."
(let (forward-sexp-function)
(save-excursion
(back-to-indentation)
(when (and (not (js2-same-line (point-min)))
(not (looking-at "{"))
(js2-re-search-backward "[[:graph:]]" nil t)
(not (looking-at "[{([]"))
(progn
(forward-char)
(when (= (char-before) ?\))
;; scan-sexps sometimes throws an error
(ignore-errors (backward-sexp))
(skip-chars-backward " \t" (point-at-bol)))
(let ((pt (point)))
(back-to-indentation)
(when (looking-at "}[ \t]*")
(goto-char (match-end 0)))
(and (looking-at js2-possibly-braceless-keywords-re)
(= (match-end 0) pt)
(not (js2-end-of-do-while-loop-p))))))
(+ (current-indentation) js2-basic-offset)))))
(defun js2-indent-in-array-comp (parse-status)
"Return non-nil if we think we're in an array comprehension.
In particular, return the buffer position of the first `for' kwd."
(let ((bracket (nth 1 parse-status))
(end (point)))
(when bracket
(save-excursion
(goto-char bracket)
(when (looking-at "\\[")
(forward-char 1)
(js2-forward-sws)
(if (looking-at "[[{]")
(let (forward-sexp-function) ; use Lisp version
(forward-sexp) ; skip destructuring form
(js2-forward-sws)
(if (and (/= (char-after) ?,) ; regular array
(looking-at "for"))
(match-beginning 0)))
;; to skip arbitrary expressions we need the parser,
;; so we'll just guess at it.
(if (and (> end (point)) ; not empty literal
(re-search-forward "[^,]]* \\(for\\) " end t)
;; not inside comment or string literal
(let ((state (parse-partial-sexp bracket (point))))
(not (or (nth 3 state) (nth 4 state)))))
(match-beginning 1))))))))
(defun js2-array-comp-indentation (parse-status for-kwd)
(if (js2-same-line for-kwd)
;; first continuation line
(save-excursion
(goto-char (nth 1 parse-status))
(forward-char 1)
(skip-chars-forward " \t")
(current-column))
(save-excursion
(goto-char for-kwd)
(current-column))))
(defun js2-maybe-goto-declaration-keyword-end (bracket)
"Helper function for `js2-proper-indentation'.
Depending on the value of `js2-pretty-multiline-declarations',
move point to the end of a variable declaration keyword so that
indentation is aligned to that column."
(cond
((eq js2-pretty-multiline-declarations 'all)
(when (looking-at js2-declaration-keyword-re)
(goto-char (1+ (match-end 0)))))
((eq js2-pretty-multiline-declarations 'dynamic)
(let (declaration-keyword-end
at-closing-bracket-p
comma-p)
(when (looking-at js2-declaration-keyword-re)
;; Preserve the match data lest it somehow be overridden.
(setq declaration-keyword-end (match-end 0))
(save-excursion
(goto-char bracket)
(setq at-closing-bracket-p
;; Handle scan errors gracefully.
(condition-case nil
(progn
;; Use the regular `forward-sexp-function' because the
;; normal one for this mode uses the AST.
(let (forward-sexp-function)
(forward-sexp))
t)
(error nil)))
(when at-closing-bracket-p
(js2-forward-sws)
(setq comma-p (looking-at-p ","))))
(when comma-p
(goto-char (1+ declaration-keyword-end))))))))
(cl-defun js2-proper-indentation (parse-status)
"Return the proper indentation for the current line."
(save-excursion
(back-to-indentation)
(when (nth 4 parse-status)
(cl-return-from js2-proper-indentation (js2--comment-indent parse-status)))
(let* ((at-closing-bracket (looking-at "[]})]"))
(same-indent-p (or at-closing-bracket
(looking-at "\\_[^:]")
(and (looking-at "\\_= js2-language-version 170)
(not (js2-same-line bracket))
(setq beg (js2-indent-in-array-comp parse-status))
(>= (point) (save-excursion
(goto-char beg)
(point-at-bol)))) ; at or after first loop?
(js2-array-comp-indentation parse-status beg))
((js2-ctrl-statement-indentation))
((and declaration-indent continued-expr-p)
(+ declaration-indent js2-basic-offset))
(declaration-indent)
(bracket
(goto-char bracket)
(cond
((looking-at "[({[][ \t]*\\(/[/*]\\|$\\)")
(when (save-excursion (skip-chars-backward " \t)")
(looking-at ")"))
(backward-list))
(back-to-indentation)
(js2-maybe-goto-declaration-keyword-end bracket)
(setq indent
(cond (same-indent-p
(current-column))
(continued-expr-p
(+ (current-column) (* 2 js2-basic-offset)))
(t
(+ (current-column) js2-basic-offset))))
(if (and js2-indent-switch-body
(not at-closing-bracket)
(looking-at "\\_"))
(+ indent js2-basic-offset)
indent))
(t
(unless same-indent-p
(forward-char)
(skip-chars-forward " \t"))
(current-column))))
(continued-expr-p js2-basic-offset)
(t 0)))))
(defun js2--comment-indent (parse-status)
"Indentation inside a multi-line block comment continuation line."
(save-excursion
(goto-char (nth 8 parse-status))
(if (looking-at "/\\*")
(+ 1 (current-column))
0)))
(defun js2-indent-line (&optional bounce-backwards)
"Indent the current line as JavaScript source text."
(interactive)
(let (parse-status offset
;; Don't whine about errors/warnings when we're indenting.
;; This has to be set before calling parse-partial-sexp below.
(inhibit-point-motion-hooks t))
(setq parse-status (save-excursion
(syntax-ppss (point-at-bol)))
offset (- (point) (save-excursion
(back-to-indentation)
(point))))
;; Don't touch multiline strings.
(unless (nth 3 parse-status)
(indent-line-to (js2-proper-indentation parse-status))
(when (cl-plusp offset)
(forward-char offset)))))
(provide 'js2-old-indent)
;;; js2-old-indent.el ends here