:safe 'booleanp
:group 'js)
+(defcustom js-jsx-syntax nil
+ "When non-nil, parse JavaScript with consideration for JSX syntax.
+This fixes indentation of JSX code in some cases. It is set to
+be buffer-local when in `js-jsx-mode'."
+ :version "27.1"
+ :type 'boolean
+ :safe 'booleanp
+ :group 'js)
+
;;; KeyMap
(defvar js-mode-map
(js--regexp-opt-symbol '("in" "instanceof")))
"Regexp matching operators that affect indentation of continued expressions.")
+(defconst js--jsx-start-tag-re
+ (concat "<" sgml-name-re)
+ "Regexp matching code that looks like a JSXOpeningElement.")
+
+(defun js--looking-at-jsx-start-tag-p ()
+ "Non-nil if a JSXOpeningElement immediately follows point."
+ (looking-at js--jsx-start-tag-re))
+
(defun js--looking-at-operator-p ()
"Return non-nil if point is on a JavaScript operator, other than a comma."
(save-match-data
(js--backward-syntactic-ws)
;; We might misindent some expressions that would
;; return NaN anyway. Shouldn't be a problem.
- (memq (char-before) '(?, ?} ?{))))))))
+ (memq (char-before) '(?, ?} ?{)))))
+ ;; “<” isn’t necessarily an operator in JSX.
+ (not (and js-jsx-syntax (js--looking-at-jsx-start-tag-p))))))
(defun js--find-newline-backward ()
"Move backward to the nearest newline that is not in a block comment."
(setq result nil)))
result))
+(defconst js--jsx-end-tag-re
+ (concat "</" sgml-name-re ">\\|/>")
+ "Regexp matching a JSXClosingElement.")
+
+(defun js--looking-back-at-jsx-end-tag-p ()
+ "Non-nil if a JSXClosingElement immediately precedes point."
+ (looking-back js--jsx-end-tag-re (point-at-bol)))
+
(defun js--continued-expression-p ()
"Return non-nil if the current line continues an expression."
(save-excursion
(and (js--find-newline-backward)
(progn
(skip-chars-backward " \t")
- (or (bobp) (backward-char))
- (and (> (point) (point-min))
- (save-excursion (backward-char) (not (looking-at "[/*]/\\|=>")))
- (js--looking-at-operator-p)
- (and (progn (backward-char)
- (not (looking-at "\\+\\+\\|--\\|/[/*]"))))))))))
+ (and
+ ;; The “>” at the end of any JSXBoundaryElement isn’t
+ ;; part of a continued expression.
+ (not (and js-jsx-syntax (js--looking-back-at-jsx-end-tag-p)))
+ (progn
+ (or (bobp) (backward-char))
+ (and (> (point) (point-min))
+ (save-excursion
+ (backward-char)
+ (not (looking-at "[/*]/\\|=>")))
+ (js--looking-at-operator-p)
+ (and (progn (backward-char)
+ (not (looking-at "\\+\\+\\|--\\|/[/*]"))))))))))))
(defun js--skip-term-backward ()
"Skip a term before point; return t if a term was skipped."
;;; JSX Indentation
-(defsubst js--jsx-find-before-tag ()
- "Find where JSX starts.
-
-Assume JSX appears in the following instances:
-- Inside parentheses, when returned or as the first argument
- to a function, and after a newline
-- When assigned to variables or object properties, but only
- on a single line
-- As the N+1th argument to a function
-
-This is an optimized version of (re-search-backward \"[(,]\n\"
-nil t), except set point to the end of the match. This logic
-executes up to the number of lines in the file, so it should be
-really fast to reduce that impact."
- (let (pos)
- (while (and (> (point) (point-min))
- (not (progn
- (end-of-line 0)
- (when (or (eq (char-before) 40) ; (
- (eq (char-before) 44)) ; ,
- (setq pos (1- (point))))))))
- pos))
-
-(defconst js--jsx-end-tag-re
- (concat "</" sgml-name-re ">\\|/>")
- "Find the end of a JSX element.")
-
-(defconst js--jsx-after-tag-re "[),]"
- "Find where JSX ends.
-This complements the assumption of where JSX appears from
-`js--jsx-before-tag-re', which see.")
-
-(defun js--jsx-indented-element-p ()
+(defmacro js--as-sgml (&rest body)
+ "Execute BODY as if in sgml-mode."
+ `(with-syntax-table sgml-mode-syntax-table
+ ,@body))
+
+(defun js--outermost-enclosing-jsx-tag-pos ()
+ (let (context tag-pos last-tag-pos parse-status parens paren-pos curly-pos)
+ (js--as-sgml
+ ;; Search until we reach the top or encounter the start of a
+ ;; JSXExpressionContainer (implying nested JSX).
+ (while (and (setq context (sgml-get-context))
+ (progn
+ (setq tag-pos (sgml-tag-start (car (last context))))
+ (or (not curly-pos)
+ ;; Stop before curly brackets (start of a
+ ;; JSXExpressionContainer).
+ (> tag-pos curly-pos))))
+ ;; Record this position so it can potentially be returned.
+ (setq last-tag-pos tag-pos)
+ ;; Always parse sexps / search for the next context from the
+ ;; immediately enclosing tag (sgml-get-context may not leave
+ ;; point there).
+ (goto-char tag-pos)
+ (unless parse-status ; Don’t needlessly reparse.
+ ;; Search upward for an enclosing starting curly bracket.
+ (setq parse-status (syntax-ppss))
+ (setq parens (reverse (nth 9 parse-status)))
+ (while (and (setq paren-pos (car parens))
+ (not (when (= (char-after paren-pos) ?{)
+ (setq curly-pos paren-pos))))
+ (setq parens (cdr parens)))
+ ;; Always search for the next context from the immediately
+ ;; enclosing tag (calling syntax-ppss in the above loop
+ ;; may move point from there).
+ (goto-char tag-pos))))
+ last-tag-pos))
+
+(defun js--jsx-indentation ()
"Determine if/how the current line should be indented as JSX.
-Return `first' for the first JSXElement on its own line.
-Return `nth' for subsequent lines of the first JSXElement.
-Return `expression' for an embedded JS expression.
-Return `after' for anything after the last JSXElement.
-Return nil for non-JSX lines.
-
-Currently, JSX indentation supports the following styles:
-
-- Single-line elements (indented like normal JS):
-
- var element = <div></div>;
-
-- Multi-line elements (enclosed in parentheses):
-
- function () {
- return (
- <div>
- <div></div>
- </div>
- );
- }
-
-- Function arguments:
-
- React.render(
- <div></div>,
- document.querySelector('.root')
- );"
+Return nil for first JSXElement line (indent like JS).
+Return `n+1th' for second+ JSXElement lines (indent like SGML).
+Return `expression' for lines within embedded JS expressions
+ (indent like JS inside SGML).
+Return nil for non-JSX lines."
(let ((current-pos (point))
(current-line (line-number-at-pos))
- last-pos
- before-tag-pos before-tag-line
- tag-start-pos tag-start-line
- tag-end-pos tag-end-line
- after-tag-line
- parens paren type)
+ tag-start-pos parens paren type)
(save-excursion
- (and
- ;; Determine if we're inside a jsx element
- (progn
- (end-of-line)
- (while (and (not tag-start-pos)
- (setq last-pos (js--jsx-find-before-tag)))
- (while (forward-comment 1))
- (when (= (char-after) 60) ; <
- (setq before-tag-pos last-pos
- tag-start-pos (point)))
- (goto-char last-pos))
- tag-start-pos)
- (progn
- (setq before-tag-line (line-number-at-pos before-tag-pos)
- tag-start-line (line-number-at-pos tag-start-pos))
- (and
- ;; A "before" line which also starts an element begins with js, so
- ;; indent it like js
- (> current-line before-tag-line)
- ;; Only indent the jsx lines like jsx
- (>= current-line tag-start-line)))
- (cond
- ;; Analyze bounds if there are any
- ((progn
- (while (and (not tag-end-pos)
- (setq last-pos (re-search-forward js--jsx-end-tag-re nil t)))
- (while (forward-comment 1))
- (when (looking-at js--jsx-after-tag-re)
- (setq tag-end-pos last-pos)))
- tag-end-pos)
- (setq tag-end-line (line-number-at-pos tag-end-pos)
- after-tag-line (line-number-at-pos after-tag-line))
- (or (and
- ;; Ensure we're actually within the bounds of the jsx
- (<= current-line tag-end-line)
- ;; An "after" line which does not end an element begins with
- ;; js, so indent it like js
- (<= current-line after-tag-line))
- (and
- ;; Handle another case where there could be e.g. comments after
- ;; the element
- (> current-line tag-end-line)
- (< current-line after-tag-line)
- (setq type 'after))))
- ;; They may not be any bounds (yet)
- (t))
- ;; Check if we're inside an embedded multi-line js expression
- (cond
- ((not type)
- (goto-char current-pos)
- (end-of-line)
- (setq parens (nth 9 (syntax-ppss)))
- (while (and parens (not type))
- (setq paren (car parens))
- (cond
- ((and (>= paren tag-start-pos)
- ;; Curly bracket indicates the start of an embedded expression
- (= (char-after paren) 123) ; {
- ;; The first line of the expression is indented like sgml
+ ;; Determine if inside a JSXElement.
+ (beginning-of-line) ; For exclusivity
+ (when (setq tag-start-pos (js--outermost-enclosing-jsx-tag-pos))
+ ;; Check if inside an embedded multi-line JS expression.
+ (goto-char current-pos)
+ (end-of-line) ; For exclusivity
+ (setq parens (nth 9 (syntax-ppss)))
+ (while
+ (and
+ (setq paren (car parens))
+ (if (and
+ (>= paren tag-start-pos)
+ ;; A curly bracket indicates the start of an
+ ;; embedded expression.
+ (= (char-after paren) ?{)
+ ;; The first line of the expression is indented
+ ;; like SGML.
(> current-line (line-number-at-pos paren))
;; Check if within a closing curly bracket (if any)
- ;; (exclusive, as the closing bracket is indented like sgml)
- (cond
- ((progn
- (goto-char paren)
- (ignore-errors (let (forward-sexp-function)
- (forward-sexp))))
- (< current-line (line-number-at-pos)))
- (t)))
- ;; Indicate this guy will be indented specially
- (setq type 'expression))
- (t (setq parens (cdr parens)))))
- t)
- (t))
- (cond
- (type)
- ;; Indent the first jsx thing like js so we can indent future jsx things
- ;; like sgml relative to the first thing
- ((= current-line tag-start-line) 'first)
- ('nth))))))
-
-(defmacro js--as-sgml (&rest body)
- "Execute BODY as if in sgml-mode."
- `(with-syntax-table sgml-mode-syntax-table
- (let (forward-sexp-function
- parse-sexp-lookup-properties)
- ,@body)))
-
-(defun js--expression-in-sgml-indent-line ()
- "Indent the current line as JavaScript or SGML (whichever is farther)."
- (let* (indent-col
- (savep (point))
- ;; 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)
- (parse-status (save-excursion
- (syntax-ppss (point-at-bol)))))
- ;; Don't touch multiline strings.
+ ;; (exclusive, as the closing bracket is indented
+ ;; like SGML).
+ (if (progn
+ (goto-char paren)
+ (ignore-errors (let (forward-sexp-function)
+ (forward-sexp))))
+ (< current-line (line-number-at-pos))
+ ;; No matching bracket implies we’re inside!
+ t))
+ ;; Indicate this will be indented specially. Return
+ ;; nil to stop iterating too.
+ (progn (setq type 'expression) nil)
+ ;; Stop iterating when parens = nil.
+ (setq parens (cdr parens)))))
+ (or type 'n+1th)))))
+
+(defun js--indent-line-in-jsx-expression ()
+ "Indent the current line as JavaScript within JSX."
+ (let ((parse-status (save-excursion (syntax-ppss (point-at-bol))))
+ offset indent-col)
(unless (nth 3 parse-status)
- (setq indent-col (save-excursion
- (back-to-indentation)
- (if (>= (point) savep) (setq savep nil))
- (js--as-sgml (sgml-calculate-indent))))
- (if (null indent-col)
- 'noindent
- ;; Use whichever indentation column is greater, such that the sgml
- ;; column is effectively a minimum
- (setq indent-col (max (js--proper-indentation parse-status)
- (+ indent-col js-indent-level)))
- (if savep
- (save-excursion (indent-line-to indent-col))
- (indent-line-to indent-col))))))
+ (save-excursion
+ (setq offset (- (point) (progn (back-to-indentation) (point)))
+ indent-col (js--as-sgml (sgml-calculate-indent))))
+ (if (null indent-col) 'noindent ; Like in sgml-mode
+ ;; Use whichever indentation column is greater, such that the
+ ;; SGML column is effectively a minimum.
+ (indent-line-to (max (js--proper-indentation parse-status)
+ (+ indent-col js-indent-level)))
+ (when (> offset 0) (forward-char offset))))))
+
+(defun js--indent-n+1th-jsx-line ()
+ "Indent the current line as JSX within JavaScript."
+ (js--as-sgml (sgml-indent-line)))
(defun js-indent-line ()
"Indent the current line as JavaScript."
i.e., customize JSX element indentation with `sgml-basic-offset',
`sgml-attribute-offset' et al."
(interactive)
- (let ((indentation-type (js--jsx-indented-element-p)))
- (cond
- ((eq indentation-type 'expression)
- (js--expression-in-sgml-indent-line))
- ((or (eq indentation-type 'first)
- (eq indentation-type 'after))
- ;; Don't treat this first thing as a continued expression (often a "<" or
- ;; ">" causes this misinterpretation)
- (cl-letf (((symbol-function #'js--continued-expression-p) 'ignore))
- (js-indent-line)))
- ((eq indentation-type 'nth)
- (js--as-sgml (sgml-indent-line)))
- (t (js-indent-line)))))
+ (let ((type (js--jsx-indentation)))
+ (if type
+ (if (eq type 'n+1th) (js--indent-n+1th-jsx-line)
+ (js--indent-line-in-jsx-expression))
+ (js-indent-line))))
;;; Filling
(setq-local sgml-basic-offset js-indent-level))
(add-hook \\='js-jsx-mode-hook #\\='set-jsx-indentation)"
:group 'js
+ (setq-local js-jsx-syntax t)
(setq-local indent-line-function #'js-jsx-indent-line))
;;;###autoload (defalias 'javascript-mode 'js-mode)