aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lisp/progmodes/js.el337
1 files changed, 141 insertions, 196 deletions
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el
index 4d91da73340..5b992535a8c 100644
--- a/lisp/progmodes/js.el
+++ b/lisp/progmodes/js.el
@@ -572,6 +572,15 @@ then the \".\"s will be lined up:
572 :safe 'booleanp 572 :safe 'booleanp
573 :group 'js) 573 :group 'js)
574 574
575(defcustom js-jsx-syntax nil
576 "When non-nil, parse JavaScript with consideration for JSX syntax.
577This fixes indentation of JSX code in some cases. It is set to
578be buffer-local when in `js-jsx-mode'."
579 :version "27.1"
580 :type 'boolean
581 :safe 'booleanp
582 :group 'js)
583
575;;; KeyMap 584;;; KeyMap
576 585
577(defvar js-mode-map 586(defvar js-mode-map
@@ -1774,6 +1783,14 @@ This performs fontification according to `js--class-styles'."
1774 (js--regexp-opt-symbol '("in" "instanceof"))) 1783 (js--regexp-opt-symbol '("in" "instanceof")))
1775 "Regexp matching operators that affect indentation of continued expressions.") 1784 "Regexp matching operators that affect indentation of continued expressions.")
1776 1785
1786(defconst js--jsx-start-tag-re
1787 (concat "<" sgml-name-re)
1788 "Regexp matching code that looks like a JSXOpeningElement.")
1789
1790(defun js--looking-at-jsx-start-tag-p ()
1791 "Non-nil if a JSXOpeningElement immediately follows point."
1792 (looking-at js--jsx-start-tag-re))
1793
1777(defun js--looking-at-operator-p () 1794(defun js--looking-at-operator-p ()
1778 "Return non-nil if point is on a JavaScript operator, other than a comma." 1795 "Return non-nil if point is on a JavaScript operator, other than a comma."
1779 (save-match-data 1796 (save-match-data
@@ -1796,7 +1813,9 @@ This performs fontification according to `js--class-styles'."
1796 (js--backward-syntactic-ws) 1813 (js--backward-syntactic-ws)
1797 ;; We might misindent some expressions that would 1814 ;; We might misindent some expressions that would
1798 ;; return NaN anyway. Shouldn't be a problem. 1815 ;; return NaN anyway. Shouldn't be a problem.
1799 (memq (char-before) '(?, ?} ?{)))))))) 1816 (memq (char-before) '(?, ?} ?{)))))
1817 ;; “<” isn’t necessarily an operator in JSX.
1818 (not (and js-jsx-syntax (js--looking-at-jsx-start-tag-p))))))
1800 1819
1801(defun js--find-newline-backward () 1820(defun js--find-newline-backward ()
1802 "Move backward to the nearest newline that is not in a block comment." 1821 "Move backward to the nearest newline that is not in a block comment."
@@ -1816,6 +1835,14 @@ This performs fontification according to `js--class-styles'."
1816 (setq result nil))) 1835 (setq result nil)))
1817 result)) 1836 result))
1818 1837
1838(defconst js--jsx-end-tag-re
1839 (concat "</" sgml-name-re ">\\|/>")
1840 "Regexp matching a JSXClosingElement.")
1841
1842(defun js--looking-back-at-jsx-end-tag-p ()
1843 "Non-nil if a JSXClosingElement immediately precedes point."
1844 (looking-back js--jsx-end-tag-re (point-at-bol)))
1845
1819(defun js--continued-expression-p () 1846(defun js--continued-expression-p ()
1820 "Return non-nil if the current line continues an expression." 1847 "Return non-nil if the current line continues an expression."
1821 (save-excursion 1848 (save-excursion
@@ -1833,12 +1860,19 @@ This performs fontification according to `js--class-styles'."
1833 (and (js--find-newline-backward) 1860 (and (js--find-newline-backward)
1834 (progn 1861 (progn
1835 (skip-chars-backward " \t") 1862 (skip-chars-backward " \t")
1836 (or (bobp) (backward-char)) 1863 (and
1837 (and (> (point) (point-min)) 1864 ;; The “>” at the end of any JSXBoundaryElement isn’t
1838 (save-excursion (backward-char) (not (looking-at "[/*]/\\|=>"))) 1865 ;; part of a continued expression.
1839 (js--looking-at-operator-p) 1866 (not (and js-jsx-syntax (js--looking-back-at-jsx-end-tag-p)))
1840 (and (progn (backward-char) 1867 (progn
1841 (not (looking-at "\\+\\+\\|--\\|/[/*]")))))))))) 1868 (or (bobp) (backward-char))
1869 (and (> (point) (point-min))
1870 (save-excursion
1871 (backward-char)
1872 (not (looking-at "[/*]/\\|=>")))
1873 (js--looking-at-operator-p)
1874 (and (progn (backward-char)
1875 (not (looking-at "\\+\\+\\|--\\|/[/*]"))))))))))))
1842 1876
1843(defun js--skip-term-backward () 1877(defun js--skip-term-backward ()
1844 "Skip a term before point; return t if a term was skipped." 1878 "Skip a term before point; return t if a term was skipped."
@@ -2153,190 +2187,108 @@ current line is the \"=>\" token."
2153 2187
2154;;; JSX Indentation 2188;;; JSX Indentation
2155 2189
2156(defsubst js--jsx-find-before-tag () 2190(defmacro js--as-sgml (&rest body)
2157 "Find where JSX starts. 2191 "Execute BODY as if in sgml-mode."
2158 2192 `(with-syntax-table sgml-mode-syntax-table
2159Assume JSX appears in the following instances: 2193 ,@body))
2160- Inside parentheses, when returned or as the first argument 2194
2161 to a function, and after a newline 2195(defun js--outermost-enclosing-jsx-tag-pos ()
2162- When assigned to variables or object properties, but only 2196 (let (context tag-pos last-tag-pos parse-status parens paren-pos curly-pos)
2163 on a single line 2197 (js--as-sgml
2164- As the N+1th argument to a function 2198 ;; Search until we reach the top or encounter the start of a
2165 2199 ;; JSXExpressionContainer (implying nested JSX).
2166This is an optimized version of (re-search-backward \"[(,]\n\" 2200 (while (and (setq context (sgml-get-context))
2167nil t), except set point to the end of the match. This logic 2201 (progn
2168executes up to the number of lines in the file, so it should be 2202 (setq tag-pos (sgml-tag-start (car (last context))))
2169really fast to reduce that impact." 2203 (or (not curly-pos)
2170 (let (pos) 2204 ;; Stop before curly brackets (start of a
2171 (while (and (> (point) (point-min)) 2205 ;; JSXExpressionContainer).
2172 (not (progn 2206 (> tag-pos curly-pos))))
2173 (end-of-line 0) 2207 ;; Record this position so it can potentially be returned.
2174 (when (or (eq (char-before) 40) ; ( 2208 (setq last-tag-pos tag-pos)
2175 (eq (char-before) 44)) ; , 2209 ;; Always parse sexps / search for the next context from the
2176 (setq pos (1- (point)))))))) 2210 ;; immediately enclosing tag (sgml-get-context may not leave
2177 pos)) 2211 ;; point there).
2178 2212 (goto-char tag-pos)
2179(defconst js--jsx-end-tag-re 2213 (unless parse-status ; Don’t needlessly reparse.
2180 (concat "</" sgml-name-re ">\\|/>") 2214 ;; Search upward for an enclosing starting curly bracket.
2181 "Find the end of a JSX element.") 2215 (setq parse-status (syntax-ppss))
2182 2216 (setq parens (reverse (nth 9 parse-status)))
2183(defconst js--jsx-after-tag-re "[),]" 2217 (while (and (setq paren-pos (car parens))
2184 "Find where JSX ends. 2218 (not (when (= (char-after paren-pos) ?{)
2185This complements the assumption of where JSX appears from 2219 (setq curly-pos paren-pos))))
2186`js--jsx-before-tag-re', which see.") 2220 (setq parens (cdr parens)))
2187 2221 ;; Always search for the next context from the immediately
2188(defun js--jsx-indented-element-p () 2222 ;; enclosing tag (calling syntax-ppss in the above loop
2223 ;; may move point from there).
2224 (goto-char tag-pos))))
2225 last-tag-pos))
2226
2227(defun js--jsx-indentation ()
2189 "Determine if/how the current line should be indented as JSX. 2228 "Determine if/how the current line should be indented as JSX.
2190 2229
2191Return `first' for the first JSXElement on its own line. 2230Return nil for first JSXElement line (indent like JS).
2192Return `nth' for subsequent lines of the first JSXElement. 2231Return `n+1th' for second+ JSXElement lines (indent like SGML).
2193Return `expression' for an embedded JS expression. 2232Return `expression' for lines within embedded JS expressions
2194Return `after' for anything after the last JSXElement. 2233 (indent like JS inside SGML).
2195Return nil for non-JSX lines. 2234Return nil for non-JSX lines."
2196
2197Currently, JSX indentation supports the following styles:
2198
2199- Single-line elements (indented like normal JS):
2200
2201 var element = <div></div>;
2202
2203- Multi-line elements (enclosed in parentheses):
2204
2205 function () {
2206 return (
2207 <div>
2208 <div></div>
2209 </div>
2210 );
2211 }
2212
2213- Function arguments:
2214
2215 React.render(
2216 <div></div>,
2217 document.querySelector('.root')
2218 );"
2219 (let ((current-pos (point)) 2235 (let ((current-pos (point))
2220 (current-line (line-number-at-pos)) 2236 (current-line (line-number-at-pos))
2221 last-pos 2237 tag-start-pos parens paren type)
2222 before-tag-pos before-tag-line
2223 tag-start-pos tag-start-line
2224 tag-end-pos tag-end-line
2225 after-tag-line
2226 parens paren type)
2227 (save-excursion 2238 (save-excursion
2228 (and 2239 ;; Determine if inside a JSXElement.
2229 ;; Determine if we're inside a jsx element 2240 (beginning-of-line) ; For exclusivity
2230 (progn 2241 (when (setq tag-start-pos (js--outermost-enclosing-jsx-tag-pos))
2231 (end-of-line) 2242 ;; Check if inside an embedded multi-line JS expression.
2232 (while (and (not tag-start-pos) 2243 (goto-char current-pos)
2233 (setq last-pos (js--jsx-find-before-tag))) 2244 (end-of-line) ; For exclusivity
2234 (while (forward-comment 1)) 2245 (setq parens (nth 9 (syntax-ppss)))
2235 (when (= (char-after) 60) ; < 2246 (while
2236 (setq before-tag-pos last-pos 2247 (and
2237 tag-start-pos (point))) 2248 (setq paren (car parens))
2238 (goto-char last-pos)) 2249 (if (and
2239 tag-start-pos) 2250 (>= paren tag-start-pos)
2240 (progn 2251 ;; A curly bracket indicates the start of an
2241 (setq before-tag-line (line-number-at-pos before-tag-pos) 2252 ;; embedded expression.
2242 tag-start-line (line-number-at-pos tag-start-pos)) 2253 (= (char-after paren) ?{)
2243 (and 2254 ;; The first line of the expression is indented
2244 ;; A "before" line which also starts an element begins with js, so 2255 ;; like SGML.
2245 ;; indent it like js
2246 (> current-line before-tag-line)
2247 ;; Only indent the jsx lines like jsx
2248 (>= current-line tag-start-line)))
2249 (cond
2250 ;; Analyze bounds if there are any
2251 ((progn
2252 (while (and (not tag-end-pos)
2253 (setq last-pos (re-search-forward js--jsx-end-tag-re nil t)))
2254 (while (forward-comment 1))
2255 (when (looking-at js--jsx-after-tag-re)
2256 (setq tag-end-pos last-pos)))
2257 tag-end-pos)
2258 (setq tag-end-line (line-number-at-pos tag-end-pos)
2259 after-tag-line (line-number-at-pos after-tag-line))
2260 (or (and
2261 ;; Ensure we're actually within the bounds of the jsx
2262 (<= current-line tag-end-line)
2263 ;; An "after" line which does not end an element begins with
2264 ;; js, so indent it like js
2265 (<= current-line after-tag-line))
2266 (and
2267 ;; Handle another case where there could be e.g. comments after
2268 ;; the element
2269 (> current-line tag-end-line)
2270 (< current-line after-tag-line)
2271 (setq type 'after))))
2272 ;; They may not be any bounds (yet)
2273 (t))
2274 ;; Check if we're inside an embedded multi-line js expression
2275 (cond
2276 ((not type)
2277 (goto-char current-pos)
2278 (end-of-line)
2279 (setq parens (nth 9 (syntax-ppss)))
2280 (while (and parens (not type))
2281 (setq paren (car parens))
2282 (cond
2283 ((and (>= paren tag-start-pos)
2284 ;; Curly bracket indicates the start of an embedded expression
2285 (= (char-after paren) 123) ; {
2286 ;; The first line of the expression is indented like sgml
2287 (> current-line (line-number-at-pos paren)) 2256 (> current-line (line-number-at-pos paren))
2288 ;; Check if within a closing curly bracket (if any) 2257 ;; Check if within a closing curly bracket (if any)
2289 ;; (exclusive, as the closing bracket is indented like sgml) 2258 ;; (exclusive, as the closing bracket is indented
2290 (cond 2259 ;; like SGML).
2291 ((progn 2260 (if (progn
2292 (goto-char paren) 2261 (goto-char paren)
2293 (ignore-errors (let (forward-sexp-function) 2262 (ignore-errors (let (forward-sexp-function)
2294 (forward-sexp)))) 2263 (forward-sexp))))
2295 (< current-line (line-number-at-pos))) 2264 (< current-line (line-number-at-pos))
2296 (t))) 2265 ;; No matching bracket implies we’re inside!
2297 ;; Indicate this guy will be indented specially 2266 t))
2298 (setq type 'expression)) 2267 ;; Indicate this will be indented specially. Return
2299 (t (setq parens (cdr parens))))) 2268 ;; nil to stop iterating too.
2300 t) 2269 (progn (setq type 'expression) nil)
2301 (t)) 2270 ;; Stop iterating when parens = nil.
2302 (cond 2271 (setq parens (cdr parens)))))
2303 (type) 2272 (or type 'n+1th)))))
2304 ;; Indent the first jsx thing like js so we can indent future jsx things 2273
2305 ;; like sgml relative to the first thing 2274(defun js--indent-line-in-jsx-expression ()
2306 ((= current-line tag-start-line) 'first) 2275 "Indent the current line as JavaScript within JSX."
2307 ('nth)))))) 2276 (let ((parse-status (save-excursion (syntax-ppss (point-at-bol))))
2308 2277 offset indent-col)
2309(defmacro js--as-sgml (&rest body)
2310 "Execute BODY as if in sgml-mode."
2311 `(with-syntax-table sgml-mode-syntax-table
2312 (let (forward-sexp-function
2313 parse-sexp-lookup-properties)
2314 ,@body)))
2315
2316(defun js--expression-in-sgml-indent-line ()
2317 "Indent the current line as JavaScript or SGML (whichever is farther)."
2318 (let* (indent-col
2319 (savep (point))
2320 ;; Don't whine about errors/warnings when we're indenting.
2321 ;; This has to be set before calling parse-partial-sexp below.
2322 (inhibit-point-motion-hooks t)
2323 (parse-status (save-excursion
2324 (syntax-ppss (point-at-bol)))))
2325 ;; Don't touch multiline strings.
2326 (unless (nth 3 parse-status) 2278 (unless (nth 3 parse-status)
2327 (setq indent-col (save-excursion 2279 (save-excursion
2328 (back-to-indentation) 2280 (setq offset (- (point) (progn (back-to-indentation) (point)))
2329 (if (>= (point) savep) (setq savep nil)) 2281 indent-col (js--as-sgml (sgml-calculate-indent))))
2330 (js--as-sgml (sgml-calculate-indent)))) 2282 (if (null indent-col) 'noindent ; Like in sgml-mode
2331 (if (null indent-col) 2283 ;; Use whichever indentation column is greater, such that the
2332 'noindent 2284 ;; SGML column is effectively a minimum.
2333 ;; Use whichever indentation column is greater, such that the sgml 2285 (indent-line-to (max (js--proper-indentation parse-status)
2334 ;; column is effectively a minimum 2286 (+ indent-col js-indent-level)))
2335 (setq indent-col (max (js--proper-indentation parse-status) 2287 (when (> offset 0) (forward-char offset))))))
2336 (+ indent-col js-indent-level))) 2288
2337 (if savep 2289(defun js--indent-n+1th-jsx-line ()
2338 (save-excursion (indent-line-to indent-col)) 2290 "Indent the current line as JSX within JavaScript."
2339 (indent-line-to indent-col)))))) 2291 (js--as-sgml (sgml-indent-line)))
2340 2292
2341(defun js-indent-line () 2293(defun js-indent-line ()
2342 "Indent the current line as JavaScript." 2294 "Indent the current line as JavaScript."
@@ -2353,19 +2305,11 @@ Currently, JSX indentation supports the following styles:
2353i.e., customize JSX element indentation with `sgml-basic-offset', 2305i.e., customize JSX element indentation with `sgml-basic-offset',
2354`sgml-attribute-offset' et al." 2306`sgml-attribute-offset' et al."
2355 (interactive) 2307 (interactive)
2356 (let ((indentation-type (js--jsx-indented-element-p))) 2308 (let ((type (js--jsx-indentation)))
2357 (cond 2309 (if type
2358 ((eq indentation-type 'expression) 2310 (if (eq type 'n+1th) (js--indent-n+1th-jsx-line)
2359 (js--expression-in-sgml-indent-line)) 2311 (js--indent-line-in-jsx-expression))
2360 ((or (eq indentation-type 'first) 2312 (js-indent-line))))
2361 (eq indentation-type 'after))
2362 ;; Don't treat this first thing as a continued expression (often a "<" or
2363 ;; ">" causes this misinterpretation)
2364 (cl-letf (((symbol-function #'js--continued-expression-p) 'ignore))
2365 (js-indent-line)))
2366 ((eq indentation-type 'nth)
2367 (js--as-sgml (sgml-indent-line)))
2368 (t (js-indent-line)))))
2369 2313
2370;;; Filling 2314;;; Filling
2371 2315
@@ -3944,6 +3888,7 @@ locally, like so:
3944 (setq-local sgml-basic-offset js-indent-level)) 3888 (setq-local sgml-basic-offset js-indent-level))
3945 (add-hook \\='js-jsx-mode-hook #\\='set-jsx-indentation)" 3889 (add-hook \\='js-jsx-mode-hook #\\='set-jsx-indentation)"
3946 :group 'js 3890 :group 'js
3891 (setq-local js-jsx-syntax t)
3947 (setq-local indent-line-function #'js-jsx-indent-line)) 3892 (setq-local indent-line-function #'js-jsx-indent-line))
3948 3893
3949;;;###autoload (defalias 'javascript-mode 'js-mode) 3894;;;###autoload (defalias 'javascript-mode 'js-mode)