diff options
| author | Jackson Ray Hamilton | 2015-10-30 23:55:24 -0700 |
|---|---|---|
| committer | Jackson Ray Hamilton | 2015-10-31 13:02:36 -0700 |
| commit | 958da7ff63d1d647f45fd649654136da78529324 (patch) | |
| tree | d971bf4c54fef8741bf4f11912508f4dd40162cb | |
| parent | 65a3808fcf0afbd90d3ae512ff1ae4395bb2ee69 (diff) | |
| download | emacs-958da7ff63d1d647f45fd649654136da78529324.tar.gz emacs-958da7ff63d1d647f45fd649654136da78529324.zip | |
Add JSX indentation via js-jsx-mode. (Bug#21799)
* progmodes/js.el: Add JSX indentation support.
(js-jsx-indent-line)
(js-jsx-mode): New functions.
| -rw-r--r-- | etc/NEWS | 3 | ||||
| -rw-r--r-- | lisp/progmodes/js.el | 221 | ||||
| -rw-r--r-- | test/indent/js-jsx.js | 85 |
3 files changed, 309 insertions, 0 deletions
| @@ -911,6 +911,9 @@ alists, hash-table and arrays. All functions are prefixed with | |||
| 911 | ** The `thunk' library provides functions and macros to control the | 911 | ** The `thunk' library provides functions and macros to control the |
| 912 | evaluation of forms. | 912 | evaluation of forms. |
| 913 | 913 | ||
| 914 | ** js-jsx-mode (a minor variant of js-mode) provides indentation | ||
| 915 | support for JSX, an XML-like syntax extension to ECMAScript | ||
| 916 | |||
| 914 | 917 | ||
| 915 | * Incompatible Lisp Changes in Emacs 25.1 | 918 | * Incompatible Lisp Changes in Emacs 25.1 |
| 916 | 919 | ||
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 5a4f383337e..3ce1c17352f 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el | |||
| @@ -52,6 +52,7 @@ | |||
| 52 | (require 'imenu) | 52 | (require 'imenu) |
| 53 | (require 'moz nil t) | 53 | (require 'moz nil t) |
| 54 | (require 'json nil t) | 54 | (require 'json nil t) |
| 55 | (require 'sgml-mode) | ||
| 55 | 56 | ||
| 56 | (eval-when-compile | 57 | (eval-when-compile |
| 57 | (require 'cl-lib) | 58 | (require 'cl-lib) |
| @@ -1998,6 +1999,193 @@ indentation is aligned to that column." | |||
| 1998 | (+ js-indent-level js-expr-indent-offset)) | 1999 | (+ js-indent-level js-expr-indent-offset)) |
| 1999 | (t 0)))) | 2000 | (t 0)))) |
| 2000 | 2001 | ||
| 2002 | ;;; JSX Indentation | ||
| 2003 | |||
| 2004 | (defsubst js--jsx-find-before-tag () | ||
| 2005 | "Find where JSX starts. | ||
| 2006 | |||
| 2007 | Assume JSX appears in the following instances: | ||
| 2008 | - Inside parentheses, when returned or as the first argument | ||
| 2009 | to a function, and after a newline | ||
| 2010 | - When assigned to variables or object properties, but only | ||
| 2011 | on a single line | ||
| 2012 | - As the N+1th argument to a function | ||
| 2013 | |||
| 2014 | This is an optimized version of (re-search-backward \"[(,]\n\" | ||
| 2015 | nil t), except set point to the end of the match. This logic | ||
| 2016 | executes up to the number of lines in the file, so it should be | ||
| 2017 | really fast to reduce that impact." | ||
| 2018 | (let (pos) | ||
| 2019 | (while (and (> (point) (point-min)) | ||
| 2020 | (not (progn | ||
| 2021 | (end-of-line 0) | ||
| 2022 | (when (or (eq (char-before) 40) ; ( | ||
| 2023 | (eq (char-before) 44)) ; , | ||
| 2024 | (setq pos (1- (point)))))))) | ||
| 2025 | pos)) | ||
| 2026 | |||
| 2027 | (defconst js--jsx-end-tag-re | ||
| 2028 | (concat "</" sgml-name-re ">\\|/>") | ||
| 2029 | "Find the end of a JSX element.") | ||
| 2030 | |||
| 2031 | (defconst js--jsx-after-tag-re "[),]" | ||
| 2032 | "Find where JSX ends. | ||
| 2033 | This complements the assumption of where JSX appears from | ||
| 2034 | `js--jsx-before-tag-re', which see.") | ||
| 2035 | |||
| 2036 | (defun js--jsx-indented-element-p () | ||
| 2037 | "Determine if/how the current line should be indented as JSX. | ||
| 2038 | |||
| 2039 | Return `first' for the first JSXElement on its own line. | ||
| 2040 | Return `nth' for subsequent lines of the first JSXElement. | ||
| 2041 | Return `expression' for an embedded JS expression. | ||
| 2042 | Return `after' for anything after the last JSXElement. | ||
| 2043 | Return nil for non-JSX lines. | ||
| 2044 | |||
| 2045 | Currently, JSX indentation supports the following styles: | ||
| 2046 | |||
| 2047 | - Single-line elements (indented like normal JS): | ||
| 2048 | |||
| 2049 | var element = <div></div>; | ||
| 2050 | |||
| 2051 | - Multi-line elements (enclosed in parentheses): | ||
| 2052 | |||
| 2053 | function () { | ||
| 2054 | return ( | ||
| 2055 | <div> | ||
| 2056 | <div></div> | ||
| 2057 | </div> | ||
| 2058 | ); | ||
| 2059 | } | ||
| 2060 | |||
| 2061 | - Function arguments: | ||
| 2062 | |||
| 2063 | React.render( | ||
| 2064 | <div></div>, | ||
| 2065 | document.querySelector('.root') | ||
| 2066 | );" | ||
| 2067 | (let ((current-pos (point)) | ||
| 2068 | (current-line (line-number-at-pos)) | ||
| 2069 | last-pos | ||
| 2070 | before-tag-pos before-tag-line | ||
| 2071 | tag-start-pos tag-start-line | ||
| 2072 | tag-end-pos tag-end-line | ||
| 2073 | after-tag-line | ||
| 2074 | parens paren type) | ||
| 2075 | (save-excursion | ||
| 2076 | (and | ||
| 2077 | ;; Determine if we're inside a jsx element | ||
| 2078 | (progn | ||
| 2079 | (end-of-line) | ||
| 2080 | (while (and (not tag-start-pos) | ||
| 2081 | (setq last-pos (js--jsx-find-before-tag))) | ||
| 2082 | (while (forward-comment 1)) | ||
| 2083 | (when (= (char-after) 60) ; < | ||
| 2084 | (setq before-tag-pos last-pos | ||
| 2085 | tag-start-pos (point))) | ||
| 2086 | (goto-char last-pos)) | ||
| 2087 | tag-start-pos) | ||
| 2088 | (progn | ||
| 2089 | (setq before-tag-line (line-number-at-pos before-tag-pos) | ||
| 2090 | tag-start-line (line-number-at-pos tag-start-pos)) | ||
| 2091 | (and | ||
| 2092 | ;; A "before" line which also starts an element begins with js, so | ||
| 2093 | ;; indent it like js | ||
| 2094 | (> current-line before-tag-line) | ||
| 2095 | ;; Only indent the jsx lines like jsx | ||
| 2096 | (>= current-line tag-start-line))) | ||
| 2097 | (cond | ||
| 2098 | ;; Analyze bounds if there are any | ||
| 2099 | ((progn | ||
| 2100 | (while (and (not tag-end-pos) | ||
| 2101 | (setq last-pos (re-search-forward js--jsx-end-tag-re nil t))) | ||
| 2102 | (while (forward-comment 1)) | ||
| 2103 | (when (looking-at js--jsx-after-tag-re) | ||
| 2104 | (setq tag-end-pos last-pos))) | ||
| 2105 | tag-end-pos) | ||
| 2106 | (setq tag-end-line (line-number-at-pos tag-end-pos) | ||
| 2107 | after-tag-line (line-number-at-pos after-tag-line)) | ||
| 2108 | (or (and | ||
| 2109 | ;; Ensure we're actually within the bounds of the jsx | ||
| 2110 | (<= current-line tag-end-line) | ||
| 2111 | ;; An "after" line which does not end an element begins with | ||
| 2112 | ;; js, so indent it like js | ||
| 2113 | (<= current-line after-tag-line)) | ||
| 2114 | (and | ||
| 2115 | ;; Handle another case where there could be e.g. comments after | ||
| 2116 | ;; the element | ||
| 2117 | (> current-line tag-end-line) | ||
| 2118 | (< current-line after-tag-line) | ||
| 2119 | (setq type 'after)))) | ||
| 2120 | ;; They may not be any bounds (yet) | ||
| 2121 | (t)) | ||
| 2122 | ;; Check if we're inside an embedded multi-line js expression | ||
| 2123 | (cond | ||
| 2124 | ((not type) | ||
| 2125 | (goto-char current-pos) | ||
| 2126 | (end-of-line) | ||
| 2127 | (setq parens (nth 9 (syntax-ppss))) | ||
| 2128 | (while (and parens (not type)) | ||
| 2129 | (setq paren (car parens)) | ||
| 2130 | (cond | ||
| 2131 | ((and (>= paren tag-start-pos) | ||
| 2132 | ;; Curly bracket indicates the start of an embedded expression | ||
| 2133 | (= (char-after paren) 123) ; { | ||
| 2134 | ;; The first line of the expression is indented like sgml | ||
| 2135 | (> current-line (line-number-at-pos paren)) | ||
| 2136 | ;; Check if within a closing curly bracket (if any) | ||
| 2137 | ;; (exclusive, as the closing bracket is indented like sgml) | ||
| 2138 | (cond | ||
| 2139 | ((progn | ||
| 2140 | (goto-char paren) | ||
| 2141 | (ignore-errors (let (forward-sexp-function) | ||
| 2142 | (forward-sexp)))) | ||
| 2143 | (< current-line (line-number-at-pos))) | ||
| 2144 | (t))) | ||
| 2145 | ;; Indicate this guy will be indented specially | ||
| 2146 | (setq type 'expression)) | ||
| 2147 | (t (setq parens (cdr parens))))) | ||
| 2148 | t) | ||
| 2149 | (t)) | ||
| 2150 | (cond | ||
| 2151 | (type) | ||
| 2152 | ;; Indent the first jsx thing like js so we can indent future jsx things | ||
| 2153 | ;; like sgml relative to the first thing | ||
| 2154 | ((= current-line tag-start-line) 'first) | ||
| 2155 | ('nth)))))) | ||
| 2156 | |||
| 2157 | (defmacro js--as-sgml (&rest body) | ||
| 2158 | "Execute BODY as if in sgml-mode." | ||
| 2159 | `(with-syntax-table sgml-mode-syntax-table | ||
| 2160 | (let (forward-sexp-function | ||
| 2161 | parse-sexp-lookup-properties) | ||
| 2162 | ,@body))) | ||
| 2163 | |||
| 2164 | (defun js--expression-in-sgml-indent-line () | ||
| 2165 | "Indent the current line as JavaScript or SGML (whichever is farther)." | ||
| 2166 | (let* (indent-col | ||
| 2167 | (savep (point)) | ||
| 2168 | ;; Don't whine about errors/warnings when we're indenting. | ||
| 2169 | ;; This has to be set before calling parse-partial-sexp below. | ||
| 2170 | (inhibit-point-motion-hooks t) | ||
| 2171 | (parse-status (save-excursion | ||
| 2172 | (syntax-ppss (point-at-bol))))) | ||
| 2173 | ;; Don't touch multiline strings. | ||
| 2174 | (unless (nth 3 parse-status) | ||
| 2175 | (setq indent-col (save-excursion | ||
| 2176 | (back-to-indentation) | ||
| 2177 | (if (>= (point) savep) (setq savep nil)) | ||
| 2178 | (js--as-sgml (sgml-calculate-indent)))) | ||
| 2179 | (if (null indent-col) | ||
| 2180 | 'noindent | ||
| 2181 | ;; Use whichever indentation column is greater, such that the sgml | ||
| 2182 | ;; column is effectively a minimum | ||
| 2183 | (setq indent-col (max (js--proper-indentation parse-status) | ||
| 2184 | (+ indent-col js-indent-level))) | ||
| 2185 | (if savep | ||
| 2186 | (save-excursion (indent-line-to indent-col)) | ||
| 2187 | (indent-line-to indent-col)))))) | ||
| 2188 | |||
| 2001 | (defun js-indent-line () | 2189 | (defun js-indent-line () |
| 2002 | "Indent the current line as JavaScript." | 2190 | "Indent the current line as JavaScript." |
| 2003 | (interactive) | 2191 | (interactive) |
| @@ -2008,6 +2196,25 @@ indentation is aligned to that column." | |||
| 2008 | (indent-line-to (js--proper-indentation parse-status)) | 2196 | (indent-line-to (js--proper-indentation parse-status)) |
| 2009 | (when (> offset 0) (forward-char offset))))) | 2197 | (when (> offset 0) (forward-char offset))))) |
| 2010 | 2198 | ||
| 2199 | (defun js-jsx-indent-line () | ||
| 2200 | "Indent the current line as JSX (with SGML offsets). | ||
| 2201 | i.e., customize JSX element indentation with `sgml-basic-offset', | ||
| 2202 | `sgml-attribute-offset' et al." | ||
| 2203 | (interactive) | ||
| 2204 | (let ((indentation-type (js--jsx-indented-element-p))) | ||
| 2205 | (cond | ||
| 2206 | ((eq indentation-type 'expression) | ||
| 2207 | (js--expression-in-sgml-indent-line)) | ||
| 2208 | ((or (eq indentation-type 'first) | ||
| 2209 | (eq indentation-type 'after)) | ||
| 2210 | ;; Don't treat this first thing as a continued expression (often a "<" or | ||
| 2211 | ;; ">" causes this misinterpretation) | ||
| 2212 | (cl-letf (((symbol-function #'js--continued-expression-p) 'ignore)) | ||
| 2213 | (js-indent-line))) | ||
| 2214 | ((eq indentation-type 'nth) | ||
| 2215 | (js--as-sgml (sgml-indent-line))) | ||
| 2216 | (t (js-indent-line))))) | ||
| 2217 | |||
| 2011 | ;;; Filling | 2218 | ;;; Filling |
| 2012 | 2219 | ||
| 2013 | (defvar js--filling-paragraph nil) | 2220 | (defvar js--filling-paragraph nil) |
| @@ -3566,6 +3773,20 @@ If one hasn't been set, or if it's stale, prompt for a new one." | |||
| 3566 | ;;(syntax-propertize (point-max)) | 3773 | ;;(syntax-propertize (point-max)) |
| 3567 | ) | 3774 | ) |
| 3568 | 3775 | ||
| 3776 | ;;;###autoload | ||
| 3777 | (define-derived-mode js-jsx-mode js-mode "JSX" | ||
| 3778 | "Major mode for editing JSX. | ||
| 3779 | |||
| 3780 | To customize the indentation for this mode, set the SGML offset | ||
| 3781 | variables (`sgml-basic-offset', `sgml-attribute-offset' et al) | ||
| 3782 | locally, like so: | ||
| 3783 | |||
| 3784 | (defun set-jsx-indentation () | ||
| 3785 | (setq-local sgml-basic-offset js-indent-level)) | ||
| 3786 | (add-hook 'js-jsx-mode-hook #'set-jsx-indentation)" | ||
| 3787 | :group 'js | ||
| 3788 | (setq-local indent-line-function #'js-jsx-indent-line)) | ||
| 3789 | |||
| 3569 | ;;;###autoload (defalias 'javascript-mode 'js-mode) | 3790 | ;;;###autoload (defalias 'javascript-mode 'js-mode) |
| 3570 | 3791 | ||
| 3571 | (eval-after-load 'folding | 3792 | (eval-after-load 'folding |
diff --git a/test/indent/js-jsx.js b/test/indent/js-jsx.js new file mode 100644 index 00000000000..7401939d282 --- /dev/null +++ b/test/indent/js-jsx.js | |||
| @@ -0,0 +1,85 @@ | |||
| 1 | // -*- mode: js-jsx; -*- | ||
| 2 | |||
| 3 | var foo = <div></div>; | ||
| 4 | |||
| 5 | return ( | ||
| 6 | <div> | ||
| 7 | </div> | ||
| 8 | <div> | ||
| 9 | <div></div> | ||
| 10 | <div> | ||
| 11 | <div></div> | ||
| 12 | </div> | ||
| 13 | </div> | ||
| 14 | ); | ||
| 15 | |||
| 16 | React.render( | ||
| 17 | <div> | ||
| 18 | <div></div> | ||
| 19 | </div>, | ||
| 20 | { | ||
| 21 | a: 1 | ||
| 22 | }, | ||
| 23 | <div> | ||
| 24 | <div></div> | ||
| 25 | </div> | ||
| 26 | ); | ||
| 27 | |||
| 28 | return ( | ||
| 29 | // Sneaky! | ||
| 30 | <div></div> | ||
| 31 | ); | ||
| 32 | |||
| 33 | return ( | ||
| 34 | <div></div> | ||
| 35 | // Sneaky! | ||
| 36 | ); | ||
| 37 | |||
| 38 | React.render( | ||
| 39 | <input | ||
| 40 | />, | ||
| 41 | { | ||
| 42 | a: 1 | ||
| 43 | } | ||
| 44 | ); | ||
| 45 | |||
| 46 | return ( | ||
| 47 | <div> | ||
| 48 | {array.map(function () { | ||
| 49 | return { | ||
| 50 | a: 1 | ||
| 51 | }; | ||
| 52 | })} | ||
| 53 | </div> | ||
| 54 | ); | ||
| 55 | |||
| 56 | return ( | ||
| 57 | <div attribute={array.map(function () { | ||
| 58 | return { | ||
| 59 | a: 1 | ||
| 60 | }; | ||
| 61 | |||
| 62 | return { | ||
| 63 | a: 1 | ||
| 64 | }; | ||
| 65 | |||
| 66 | return { | ||
| 67 | a: 1 | ||
| 68 | }; | ||
| 69 | })}> | ||
| 70 | </div> | ||
| 71 | ); | ||
| 72 | |||
| 73 | // Local Variables: | ||
| 74 | // indent-tabs-mode: nil | ||
| 75 | // js-indent-level: 2 | ||
| 76 | // End: | ||
| 77 | |||
| 78 | // The following test has intentionally unclosed elements and should | ||
| 79 | // be placed below all other tests to prevent awkward indentation. | ||
| 80 | |||
| 81 | return ( | ||
| 82 | <div> | ||
| 83 | {array.map(function () { | ||
| 84 | return { | ||
| 85 | a: 1 | ||