diff options
| -rw-r--r-- | lisp/progmodes/js.el | 337 |
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. | ||
| 577 | This fixes indentation of JSX code in some cases. It is set to | ||
| 578 | be 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 | |
| 2159 | Assume 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). | |
| 2166 | This is an optimized version of (re-search-backward \"[(,]\n\" | 2200 | (while (and (setq context (sgml-get-context)) |
| 2167 | nil t), except set point to the end of the match. This logic | 2201 | (progn |
| 2168 | executes up to the number of lines in the file, so it should be | 2202 | (setq tag-pos (sgml-tag-start (car (last context)))) |
| 2169 | really 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) ?{) |
| 2185 | This 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 | ||
| 2191 | Return `first' for the first JSXElement on its own line. | 2230 | Return nil for first JSXElement line (indent like JS). |
| 2192 | Return `nth' for subsequent lines of the first JSXElement. | 2231 | Return `n+1th' for second+ JSXElement lines (indent like SGML). |
| 2193 | Return `expression' for an embedded JS expression. | 2232 | Return `expression' for lines within embedded JS expressions |
| 2194 | Return `after' for anything after the last JSXElement. | 2233 | (indent like JS inside SGML). |
| 2195 | Return nil for non-JSX lines. | 2234 | Return nil for non-JSX lines." |
| 2196 | |||
| 2197 | Currently, 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: | |||
| 2353 | i.e., customize JSX element indentation with `sgml-basic-offset', | 2305 | i.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) |